mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-04-25 10:22:11 +00:00
Compare commits
3 Commits
fix/fullsc
...
fix/ci-she
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
678c4ce8da | ||
|
|
752f25382a | ||
|
|
092d0809f0 |
1
.badges/backend-coverage.json
Normal file
1
.badges/backend-coverage.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"schemaVersion":1,"label":"backend coverage","message":"87.79%","color":"brightgreen"}
|
||||||
1
.badges/backend-tests.json
Normal file
1
.badges/backend-tests.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"schemaVersion":1,"label":"backend tests","message":"998 passed","color":"brightgreen"}
|
||||||
1
.badges/coverage.json
Normal file
1
.badges/coverage.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"schemaVersion":1,"label":"coverage","message":"76%","color":"yellow"}
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"schemaVersion":1,"label":"e2e tests","message":"45 passed","color":"brightgreen"}
|
|
||||||
@@ -1 +1 @@
|
|||||||
{"schemaVersion":1,"label":"frontend coverage","message":"39.68%","color":"red"}
|
{"schemaVersion":1,"label":"frontend coverage","message":"31.35%","color":"red"}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{"schemaVersion":1,"label":"go ingestor coverage","message":"70.2%","color":"yellow"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"schemaVersion":1,"label":"go server coverage","message":"85.4%","color":"green"}
|
|
||||||
1
.badges/tests.json
Normal file
1
.badges/tests.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"schemaVersion":1,"label":"tests","message":"844/844 passed","color":"brightgreen"}
|
||||||
95
.env.example
95
.env.example
@@ -1,51 +1,44 @@
|
|||||||
# MeshCore Analyzer — Environment Configuration
|
# MeshCore Analyzer — Environment Configuration
|
||||||
# Copy to .env and customize. All values have sensible defaults.
|
# Copy to .env and customize. All values have sensible defaults.
|
||||||
#
|
#
|
||||||
# This file is read by BOTH docker compose AND manage.sh — one source of truth.
|
# This file is read by BOTH docker compose AND manage.sh — one source of truth.
|
||||||
# manage.sh setup negotiates and updates only these production managed keys:
|
# Each environment keeps config + data together in one directory:
|
||||||
# PROD_DATA_DIR, PROD_HTTP_PORT, PROD_HTTPS_PORT, PROD_MQTT_PORT, DISABLE_MOSQUITTO
|
# ~/meshcore-data/config.json, meshcore.db, Caddyfile, theme.json
|
||||||
# Each environment keeps config + data together in one directory:
|
# ~/meshcore-staging-data/config.json, meshcore.db, Caddyfile
|
||||||
# ~/meshcore-data/config.json, meshcore.db, Caddyfile, theme.json
|
|
||||||
# ~/meshcore-staging-data/config.json, meshcore.db, Caddyfile
|
# --- Production ---
|
||||||
|
# Data directory (database, theme, etc.)
|
||||||
# --- Production ---
|
# Default: ~/meshcore-data
|
||||||
# Data directory (database, theme, etc.)
|
# Used by: docker compose, manage.sh
|
||||||
# Default: ~/meshcore-data
|
PROD_DATA_DIR=~/meshcore-data
|
||||||
# Used by: docker compose, manage.sh
|
|
||||||
PROD_DATA_DIR=~/meshcore-data
|
# HTTP port for web UI
|
||||||
|
# Default: 80
|
||||||
# HTTP port for web UI
|
# Used by: docker compose
|
||||||
# Default: 80
|
PROD_HTTP_PORT=80
|
||||||
# Used by: docker compose
|
|
||||||
PROD_HTTP_PORT=80
|
# HTTPS port for web UI (TLS via Caddy)
|
||||||
|
# Default: 443
|
||||||
# HTTPS port for web UI (TLS via Caddy)
|
# Used by: docker compose
|
||||||
# Default: 443
|
PROD_HTTPS_PORT=443
|
||||||
# Used by: docker compose
|
|
||||||
PROD_HTTPS_PORT=443
|
# MQTT port for observer connections
|
||||||
|
# Default: 1883
|
||||||
# MQTT port for observer connections
|
# Used by: docker compose
|
||||||
# Default: 1883
|
PROD_MQTT_PORT=1883
|
||||||
# Used by: docker compose
|
|
||||||
PROD_MQTT_PORT=1883
|
# --- Staging (HTTP only, no HTTPS) ---
|
||||||
|
# Data directory
|
||||||
# Disable internal Mosquitto broker (set true to use external MQTT only)
|
# Default: ~/meshcore-staging-data
|
||||||
# Default: false
|
# Used by: docker compose
|
||||||
# Used by: manage.sh + docker compose overrides
|
STAGING_DATA_DIR=~/meshcore-staging-data
|
||||||
DISABLE_MOSQUITTO=false
|
|
||||||
|
# HTTP port
|
||||||
# --- Staging (HTTP only, no HTTPS) ---
|
# Default: 81
|
||||||
# Data directory
|
# Used by: docker compose
|
||||||
# Default: ~/meshcore-staging-data
|
STAGING_HTTP_PORT=81
|
||||||
# Used by: docker compose
|
|
||||||
STAGING_DATA_DIR=~/meshcore-staging-data
|
# MQTT port
|
||||||
|
# Default: 1884
|
||||||
# HTTP port
|
# Used by: docker compose
|
||||||
# Default: 82
|
STAGING_MQTT_PORT=1884
|
||||||
# Used by: docker compose
|
|
||||||
STAGING_GO_HTTP_PORT=82
|
|
||||||
|
|
||||||
# MQTT port
|
|
||||||
# Default: 1885
|
|
||||||
# Used by: docker compose
|
|
||||||
STAGING_GO_MQTT_PORT=1885
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
# Line ending normalization (CRLF → LF) — no functional changes
|
|
||||||
b6e4ebf12eba21c78b72978e55052307ca72dbc1
|
|
||||||
17
.gitattributes
vendored
17
.gitattributes
vendored
@@ -1,17 +0,0 @@
|
|||||||
# Force LF line endings for all text files (prevents CRLF churn from Windows agents)
|
|
||||||
* text=auto eol=lf
|
|
||||||
|
|
||||||
# Explicitly mark binary files
|
|
||||||
*.png binary
|
|
||||||
*.jpg binary
|
|
||||||
*.ico binary
|
|
||||||
*.db binary
|
|
||||||
|
|
||||||
# Squad: union merge for append-only team state files
|
|
||||||
.squad/decisions.md merge=union
|
|
||||||
.squad/agents/*/history.md merge=union
|
|
||||||
.squad/log/** merge=union
|
|
||||||
.squad/orchestration-log/** merge=union
|
|
||||||
|
|
||||||
manage.sh text eol=lf
|
|
||||||
*.sh text eol=lf
|
|
||||||
1287
.github/agents/squad.agent.md
vendored
1287
.github/agents/squad.agent.md
vendored
File diff suppressed because it is too large
Load Diff
122
.github/instructions/copilot.instructions.md
vendored
122
.github/instructions/copilot.instructions.md
vendored
@@ -1,61 +1,61 @@
|
|||||||
---
|
---
|
||||||
name: "MeshCore PR Reviewer"
|
name: "MeshCore PR Reviewer"
|
||||||
description: "A specialized agent for reviewing pull requests in the meshcore-analyzer repository. It focuses on SOLID, DRY, testing, Go best practices, frontend testability, observability, and performance to prevent regressions and maintain high code quality."
|
description: "A specialized agent for reviewing pull requests in the meshcore-analyzer repository. It focuses on SOLID, DRY, testing, Go best practices, frontend testability, observability, and performance to prevent regressions and maintain high code quality."
|
||||||
model: "gpt-5.3-codex"
|
model: "gpt-5.3-codex"
|
||||||
tools: ["githubread", "add_issue_comment"]
|
tools: ["githubread", "add_issue_comment"]
|
||||||
---
|
---
|
||||||
|
|
||||||
# MeshCore PR Reviewer Agent
|
# MeshCore PR Reviewer Agent
|
||||||
|
|
||||||
You are an expert software engineer specializing in Go and JavaScript-heavy network analysis tools. Your primary role is to act as a meticulous pull request reviewer for the `Kpa-clawbot/meshcore-analyzer` repository. You are deeply familiar with its architecture, as outlined in `AGENTS.md`, and you enforce its rules rigorously.
|
You are an expert software engineer specializing in Go and JavaScript-heavy network analysis tools. Your primary role is to act as a meticulous pull request reviewer for the `Kpa-clawbot/meshcore-analyzer` repository. You are deeply familiar with its architecture, as outlined in `AGENTS.md`, and you enforce its rules rigorously.
|
||||||
|
|
||||||
Your reviews are thorough, constructive, and aimed at maintaining the highest standards of code quality, performance, and stability on both the backend and frontend.
|
Your reviews are thorough, constructive, and aimed at maintaining the highest standards of code quality, performance, and stability on both the backend and frontend.
|
||||||
|
|
||||||
## Core Principles
|
## Core Principles
|
||||||
|
|
||||||
1. **Context is King**: Before any review, consult the `AGENTS.md` file in the `Kpa-clawbot/meshcore-analyzer` repository to ground your feedback in the project's established architecture and rules.
|
1. **Context is King**: Before any review, consult the `AGENTS.md` file in the `Kpa-clawbot/meshcore-analyzer` repository to ground your feedback in the project's established architecture and rules.
|
||||||
2. **Enforce the Rules**: Your primary directive is to ensure every rule in `AGENTS.md` is followed. Call out any deviation.
|
2. **Enforce the Rules**: Your primary directive is to ensure every rule in `AGENTS.md` is followed. Call out any deviation.
|
||||||
3. **Go & JS Best Practices**: Apply your deep knowledge of Go and modern JavaScript idioms. Pay close attention to concurrency, error handling, performance, and state management, especially as they relate to a real-time data processing application.
|
3. **Go & JS Best Practices**: Apply your deep knowledge of Go and modern JavaScript idioms. Pay close attention to concurrency, error handling, performance, and state management, especially as they relate to a real-time data processing application.
|
||||||
4. **Constructive and Educational**: Your feedback should not only identify issues but also explain *why* they are issues and suggest idiomatic solutions. Your goal is to mentor and elevate the codebase and its contributors.
|
4. **Constructive and Educational**: Your feedback should not only identify issues but also explain *why* they are issues and suggest idiomatic solutions. Your goal is to mentor and elevate the codebase and its contributors.
|
||||||
5. **Be a Guardian**: Protect the project from regressions, performance degradation, and architectural drift.
|
5. **Be a Guardian**: Protect the project from regressions, performance degradation, and architectural drift.
|
||||||
|
|
||||||
## Review Focus Areas
|
## Review Focus Areas
|
||||||
|
|
||||||
You will pay special attention to the following areas during your review:
|
You will pay special attention to the following areas during your review:
|
||||||
|
|
||||||
### 1. Architectural Adherence & Design Principles
|
### 1. Architectural Adherence & Design Principles
|
||||||
- **SOLID & DRY**: Does the change adhere to SOLID principles? Is there duplicated logic that could be refactored? Does it respect the existing separation of concerns?
|
- **SOLID & DRY**: Does the change adhere to SOLID principles? Is there duplicated logic that could be refactored? Does it respect the existing separation of concerns?
|
||||||
- **Project Architecture**: Does the PR respect the single Node.js server + static frontend architecture? Are changes in the right place?
|
- **Project Architecture**: Does the PR respect the single Node.js server + static frontend architecture? Are changes in the right place?
|
||||||
|
|
||||||
### 2. Testing and Validation
|
### 2. Testing and Validation
|
||||||
- **No commit without tests**: Is the backend logic change covered by unit tests? Is `test-packet-filter.js` or `test-aging.js` updated if necessary?
|
- **No commit without tests**: Is the backend logic change covered by unit tests? Is `test-packet-filter.js` or `test-aging.js` updated if necessary?
|
||||||
- **Browser Validation**: Has the contributor confirmed the change works in a browser? Is there a screenshot for visual changes?
|
- **Browser Validation**: Has the contributor confirmed the change works in a browser? Is there a screenshot for visual changes?
|
||||||
- **Cache Busters**: If any `public/` assets (`.js`, `.css`) were modified, has the cache buster in `public/index.html` been bumped in the *same commit*? This is critical.
|
- **Cache Busters**: If any `public/` assets (`.js`, `.css`) were modified, has the cache buster in `public/index.html` been bumped in the *same commit*? This is critical.
|
||||||
|
|
||||||
### 3. Go-Specific Concerns
|
### 3. Go-Specific Concerns
|
||||||
- **Concurrency**: Are goroutines used safely? Are there potential race conditions? Is synchronization used correctly?
|
- **Concurrency**: Are goroutines used safely? Are there potential race conditions? Is synchronization used correctly?
|
||||||
- **Error Handling**: Is error handling explicit and clear? Are errors wrapped with context where appropriate?
|
- **Error Handling**: Is error handling explicit and clear? Are errors wrapped with context where appropriate?
|
||||||
- **Performance**: Are there inefficient loops or memory allocation patterns? Scrutinize any new data processing logic.
|
- **Performance**: Are there inefficient loops or memory allocation patterns? Scrutinize any new data processing logic.
|
||||||
- **Go Idioms**: Does the code follow standard Go idioms and formatting (`gofmt`)?
|
- **Go Idioms**: Does the code follow standard Go idioms and formatting (`gofmt`)?
|
||||||
|
|
||||||
### 4. Frontend and UI Testability
|
### 4. Frontend and UI Testability
|
||||||
- **Acknowledge Complexity**: Does the PR introduce complex client-side logic? Recognize that browser-based functionality is difficult to unit test.
|
- **Acknowledge Complexity**: Does the PR introduce complex client-side logic? Recognize that browser-based functionality is difficult to unit test.
|
||||||
- **Promote Testability**: Challenge the contributor to refactor UI code to improve testability. Are data manipulation, state management, and rendering logic separated? Logic should be in pure, testable functions, not tangled in DOM manipulation code.
|
- **Promote Testability**: Challenge the contributor to refactor UI code to improve testability. Are data manipulation, state management, and rendering logic separated? Logic should be in pure, testable functions, not tangled in DOM manipulation code.
|
||||||
- **UI Logic Purity**: Scrutinize client-side JavaScript. Are there large, monolithic functions? Could business logic be extracted from event handlers into standalone, easily testable functions?
|
- **UI Logic Purity**: Scrutinize client-side JavaScript. Are there large, monolithic functions? Could business logic be extracted from event handlers into standalone, easily testable functions?
|
||||||
- **State Management**: How is client-side state managed? Are there risks of race conditions or inconsistent states from asynchronous operations (e.g., API calls)?
|
- **State Management**: How is client-side state managed? Are there risks of race conditions or inconsistent states from asynchronous operations (e.g., API calls)?
|
||||||
|
|
||||||
### 5. Observability and Maintainability
|
### 5. Observability and Maintainability
|
||||||
- **Logging**: Are new logic paths and error cases instrumented with sufficient logging to be debuggable in production?
|
- **Logging**: Are new logic paths and error cases instrumented with sufficient logging to be debuggable in production?
|
||||||
- **Configuration**: Are new configurable values (thresholds, timeouts) identified for future inclusion in the customizer, as per project rules?
|
- **Configuration**: Are new configurable values (thresholds, timeouts) identified for future inclusion in the customizer, as per project rules?
|
||||||
- **Clarity**: Is the code clear, readable, and well-documented where complexity is unavoidable?
|
- **Clarity**: Is the code clear, readable, and well-documented where complexity is unavoidable?
|
||||||
|
|
||||||
### 6. API and Data Integrity
|
### 6. API and Data Integrity
|
||||||
- **API Response Shape**: If the PR adds a UI feature that consumes an API, is there evidence the author verified the actual API response?
|
- **API Response Shape**: If the PR adds a UI feature that consumes an API, is there evidence the author verified the actual API response?
|
||||||
- **Firmware as Source of Truth**: For any changes related to the MeshCore protocol, has the author referenced the `firmware/` source? Challenge any "magic numbers" or assumptions about packet structure.
|
- **Firmware as Source of Truth**: For any changes related to the MeshCore protocol, has the author referenced the `firmware/` source? Challenge any "magic numbers" or assumptions about packet structure.
|
||||||
|
|
||||||
## Review Process
|
## Review Process
|
||||||
|
|
||||||
1. **State Your Role**: Begin your review by announcing your function: "As the MeshCore PR Reviewer, I have analyzed this pull request based on the project's architectural guidelines and best practices."
|
1. **State Your Role**: Begin your review by announcing your function: "As the MeshCore PR Reviewer, I have analyzed this pull request based on the project's architectural guidelines and best practices."
|
||||||
2. **Provide a Summary**: Give a high-level summary of your findings (e.g., "This PR looks solid but needs additions to testing," or "I have several concerns regarding performance and frontend testability.").
|
2. **Provide a Summary**: Give a high-level summary of your findings (e.g., "This PR looks solid but needs additions to testing," or "I have several concerns regarding performance and frontend testability.").
|
||||||
3. **Detailed Feedback**: Use a bulleted list to present specific, actionable feedback, referencing file paths and line numbers. For each point, cite the relevant principle or project rule (e.g., "Missing Test Coverage (Rule #1)", "UI Logic Purity (Focus Area #4)").
|
3. **Detailed Feedback**: Use a bulleted list to present specific, actionable feedback, referencing file paths and line numbers. For each point, cite the relevant principle or project rule (e.g., "Missing Test Coverage (Rule #1)", "UI Logic Purity (Focus Area #4)").
|
||||||
4. **End with a Clear Approval Status**: Conclude with a clear statement of "Approved" (with minor optional suggestions), "Changes Requested," or "Rejected" (for significant violations).
|
4. **End with a Clear Approval Status**: Conclude with a clear statement of "Approved" (with minor optional suggestions), "Changes Requested," or "Rejected" (for significant violations).
|
||||||
|
|||||||
925
.github/workflows/deploy.yml
vendored
925
.github/workflows/deploy.yml
vendored
@@ -1,514 +1,411 @@
|
|||||||
name: CI/CD Pipeline
|
name: Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
tags: ['v*']
|
paths-ignore:
|
||||||
pull_request:
|
- '**.md'
|
||||||
branches: [master]
|
- 'LICENSE'
|
||||||
workflow_dispatch:
|
- '.gitignore'
|
||||||
|
- 'docs/**'
|
||||||
permissions:
|
pull_request:
|
||||||
contents: read
|
branches: [master]
|
||||||
packages: write
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
concurrency:
|
- 'LICENSE'
|
||||||
group: ci-${{ github.event.pull_request.number || github.ref }}
|
- '.gitignore'
|
||||||
cancel-in-progress: true
|
- 'docs/**'
|
||||||
|
|
||||||
env:
|
concurrency:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
group: deploy-${{ github.event.pull_request.number || github.ref }}
|
||||||
STAGING_COMPOSE_FILE: docker-compose.staging.yml
|
cancel-in-progress: true
|
||||||
STAGING_SERVICE: staging-go
|
|
||||||
STAGING_CONTAINER: corescope-staging-go
|
defaults:
|
||||||
|
run:
|
||||||
# Pipeline (sequential, fail-fast):
|
shell: bash
|
||||||
# go-test → e2e-test → build-and-publish → deploy → publish-badges
|
|
||||||
# PRs stop after build-and-publish (no GHCR push). Master continues to deploy + badges.
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
jobs:
|
|
||||||
# ───────────────────────────────────────────────────────────────
|
# Pipeline:
|
||||||
# 1. Go Build & Test
|
# node-test (frontend tests) ──┐
|
||||||
# ───────────────────────────────────────────────────────────────
|
# go-test ├──→ build → deploy → publish
|
||||||
go-test:
|
# └─ (both wait)
|
||||||
name: "✅ Go Build & Test"
|
#
|
||||||
runs-on: ubuntu-latest
|
# Proto validation flow:
|
||||||
steps:
|
# 1. go-test job: verify .proto files compile (syntax check)
|
||||||
- name: Checkout code
|
# 2. deploy job: capture fresh fixtures from prod, validate protos match actual API responses
|
||||||
uses: actions/checkout@v5
|
|
||||||
with:
|
jobs:
|
||||||
fetch-depth: 0
|
# ───────────────────────────────────────────────────────────────
|
||||||
|
# 1. Go Build & Test — compiles + tests Go modules, coverage badges
|
||||||
- name: Clean Go module cache
|
# ───────────────────────────────────────────────────────────────
|
||||||
run: rm -rf ~/go/pkg/mod 2>/dev/null || true
|
go-test:
|
||||||
|
name: "✅ Go Build & Test"
|
||||||
- name: Set up Go 1.22
|
runs-on: ubuntu-latest
|
||||||
uses: actions/setup-go@v6
|
steps:
|
||||||
with:
|
- name: Checkout code
|
||||||
go-version: '1.22'
|
uses: actions/checkout@v5
|
||||||
cache-dependency-path: |
|
|
||||||
cmd/server/go.sum
|
- name: Set up Go 1.22
|
||||||
cmd/ingestor/go.sum
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
- name: Build and test Go server (with coverage)
|
go-version: '1.22'
|
||||||
run: |
|
cache-dependency-path: |
|
||||||
set -e -o pipefail
|
cmd/server/go.sum
|
||||||
cd cmd/server
|
cmd/ingestor/go.sum
|
||||||
go build .
|
|
||||||
go test -coverprofile=server-coverage.out ./... 2>&1 | tee server-test.log
|
- name: Build and test Go server (with coverage)
|
||||||
echo "--- Go Server Coverage ---"
|
run: |
|
||||||
go tool cover -func=server-coverage.out | tail -1
|
set -e -o pipefail
|
||||||
|
cd cmd/server
|
||||||
- name: Build and test Go ingestor (with coverage)
|
go build .
|
||||||
run: |
|
go test -coverprofile=server-coverage.out ./... 2>&1 | tee server-test.log
|
||||||
set -e -o pipefail
|
echo "--- Go Server Coverage ---"
|
||||||
cd cmd/ingestor
|
go tool cover -func=server-coverage.out | tail -1
|
||||||
go build .
|
|
||||||
go test -coverprofile=ingestor-coverage.out ./... 2>&1 | tee ingestor-test.log
|
- name: Build and test Go ingestor (with coverage)
|
||||||
echo "--- Go Ingestor Coverage ---"
|
run: |
|
||||||
go tool cover -func=ingestor-coverage.out | tail -1
|
set -e -o pipefail
|
||||||
|
cd cmd/ingestor
|
||||||
- name: Build and test channel library + decrypt CLI
|
go build .
|
||||||
run: |
|
go test -coverprofile=ingestor-coverage.out ./... 2>&1 | tee ingestor-test.log
|
||||||
set -e -o pipefail
|
echo "--- Go Ingestor Coverage ---"
|
||||||
cd internal/channel
|
go tool cover -func=ingestor-coverage.out | tail -1
|
||||||
go test ./...
|
|
||||||
echo "--- Channel library tests passed ---"
|
- name: Verify proto syntax (all .proto files compile)
|
||||||
cd ../../cmd/decrypt
|
run: |
|
||||||
CGO_ENABLED=0 go build -ldflags="-s -w" -o corescope-decrypt .
|
set -e
|
||||||
go test ./...
|
echo "Installing protoc..."
|
||||||
echo "--- Decrypt CLI tests passed ---"
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y protobuf-compiler
|
||||||
- name: Verify proto syntax
|
|
||||||
run: |
|
echo "Checking proto syntax..."
|
||||||
set -e
|
for proto in proto/*.proto; do
|
||||||
sudo apt-get update -qq
|
echo " ✓ $(basename "$proto")"
|
||||||
sudo apt-get install -y protobuf-compiler
|
protoc --proto_path=proto --descriptor_set_out=/dev/null "$proto"
|
||||||
for proto in proto/*.proto; do
|
done
|
||||||
echo " ✓ $(basename "$proto")"
|
echo "✅ All .proto files are syntactically valid"
|
||||||
protoc --proto_path=proto --descriptor_set_out=/dev/null "$proto"
|
|
||||||
done
|
- name: Generate Go coverage badges
|
||||||
echo "✅ All .proto files are syntactically valid"
|
if: always()
|
||||||
|
run: |
|
||||||
- name: Generate Go coverage badges
|
mkdir -p .badges
|
||||||
if: success()
|
|
||||||
run: |
|
# Parse server coverage
|
||||||
mkdir -p .badges
|
SERVER_COV="0"
|
||||||
|
if [ -f cmd/server/server-coverage.out ]; then
|
||||||
SERVER_COV="0"
|
SERVER_COV=$(cd cmd/server && go tool cover -func=server-coverage.out | tail -1 | grep -oP '[\d.]+(?=%)')
|
||||||
if [ -f cmd/server/server-coverage.out ]; then
|
fi
|
||||||
SERVER_COV=$(cd cmd/server && go tool cover -func=server-coverage.out | tail -1 | grep -oP '[\d.]+(?=%)')
|
SERVER_COLOR="red"
|
||||||
fi
|
if [ "$(echo "$SERVER_COV >= 80" | bc -l 2>/dev/null)" = "1" ]; then
|
||||||
SERVER_COLOR="red"
|
SERVER_COLOR="green"
|
||||||
if [ "$(echo "$SERVER_COV >= 80" | bc -l 2>/dev/null)" = "1" ]; then SERVER_COLOR="green"
|
elif [ "$(echo "$SERVER_COV >= 60" | bc -l 2>/dev/null)" = "1" ]; then
|
||||||
elif [ "$(echo "$SERVER_COV >= 60" | bc -l 2>/dev/null)" = "1" ]; then SERVER_COLOR="yellow"; fi
|
SERVER_COLOR="yellow"
|
||||||
echo "{\"schemaVersion\":1,\"label\":\"go server coverage\",\"message\":\"${SERVER_COV}%\",\"color\":\"${SERVER_COLOR}\"}" > .badges/go-server-coverage.json
|
fi
|
||||||
|
echo "{\"schemaVersion\":1,\"label\":\"go server coverage\",\"message\":\"${SERVER_COV}%\",\"color\":\"${SERVER_COLOR}\"}" > .badges/go-server-coverage.json
|
||||||
INGESTOR_COV="0"
|
echo "Go server coverage: ${SERVER_COV}% (${SERVER_COLOR})"
|
||||||
if [ -f cmd/ingestor/ingestor-coverage.out ]; then
|
|
||||||
INGESTOR_COV=$(cd cmd/ingestor && go tool cover -func=ingestor-coverage.out | tail -1 | grep -oP '[\d.]+(?=%)')
|
# Parse ingestor coverage
|
||||||
fi
|
INGESTOR_COV="0"
|
||||||
INGESTOR_COLOR="red"
|
if [ -f cmd/ingestor/ingestor-coverage.out ]; then
|
||||||
if [ "$(echo "$INGESTOR_COV >= 80" | bc -l 2>/dev/null)" = "1" ]; then INGESTOR_COLOR="green"
|
INGESTOR_COV=$(cd cmd/ingestor && go tool cover -func=ingestor-coverage.out | tail -1 | grep -oP '[\d.]+(?=%)')
|
||||||
elif [ "$(echo "$INGESTOR_COV >= 60" | bc -l 2>/dev/null)" = "1" ]; then INGESTOR_COLOR="yellow"; fi
|
fi
|
||||||
echo "{\"schemaVersion\":1,\"label\":\"go ingestor coverage\",\"message\":\"${INGESTOR_COV}%\",\"color\":\"${INGESTOR_COLOR}\"}" > .badges/go-ingestor-coverage.json
|
INGESTOR_COLOR="red"
|
||||||
|
if [ "$(echo "$INGESTOR_COV >= 80" | bc -l 2>/dev/null)" = "1" ]; then
|
||||||
echo "## Go Coverage" >> $GITHUB_STEP_SUMMARY
|
INGESTOR_COLOR="green"
|
||||||
echo "| Module | Coverage |" >> $GITHUB_STEP_SUMMARY
|
elif [ "$(echo "$INGESTOR_COV >= 60" | bc -l 2>/dev/null)" = "1" ]; then
|
||||||
echo "|--------|----------|" >> $GITHUB_STEP_SUMMARY
|
INGESTOR_COLOR="yellow"
|
||||||
echo "| Server | ${SERVER_COV}% |" >> $GITHUB_STEP_SUMMARY
|
fi
|
||||||
echo "| Ingestor | ${INGESTOR_COV}% |" >> $GITHUB_STEP_SUMMARY
|
echo "{\"schemaVersion\":1,\"label\":\"go ingestor coverage\",\"message\":\"${INGESTOR_COV}%\",\"color\":\"${INGESTOR_COLOR}\"}" > .badges/go-ingestor-coverage.json
|
||||||
|
echo "Go ingestor coverage: ${INGESTOR_COV}% (${INGESTOR_COLOR})"
|
||||||
- name: Upload Go coverage badges
|
|
||||||
if: success()
|
echo "## Go Coverage" >> $GITHUB_STEP_SUMMARY
|
||||||
uses: actions/upload-artifact@v6
|
echo "| Module | Coverage |" >> $GITHUB_STEP_SUMMARY
|
||||||
with:
|
echo "|--------|----------|" >> $GITHUB_STEP_SUMMARY
|
||||||
name: go-badges
|
echo "| Server | ${SERVER_COV}% |" >> $GITHUB_STEP_SUMMARY
|
||||||
path: .badges/go-*.json
|
echo "| Ingestor | ${INGESTOR_COV}% |" >> $GITHUB_STEP_SUMMARY
|
||||||
retention-days: 1
|
|
||||||
if-no-files-found: ignore
|
- name: Cancel workflow on failure
|
||||||
include-hidden-files: true
|
if: failure()
|
||||||
|
run: |
|
||||||
# ───────────────────────────────────────────────────────────────
|
curl -s -X POST \
|
||||||
# 2. Playwright E2E Tests (against Go server with fixture DB)
|
-H "Authorization: Bearer ${{ github.token }}" \
|
||||||
# ───────────────────────────────────────────────────────────────
|
"https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/cancel"
|
||||||
e2e-test:
|
|
||||||
name: "🎭 Playwright E2E Tests"
|
- name: Upload Go coverage badges
|
||||||
needs: [go-test]
|
if: always()
|
||||||
runs-on: [self-hosted, Linux]
|
uses: actions/upload-artifact@v5
|
||||||
defaults:
|
with:
|
||||||
run:
|
name: go-badges
|
||||||
shell: bash
|
path: .badges/go-*.json
|
||||||
steps:
|
retention-days: 1
|
||||||
- name: Checkout code
|
if-no-files-found: ignore
|
||||||
uses: actions/checkout@v5
|
|
||||||
with:
|
# ───────────────────────────────────────────────────────────────
|
||||||
fetch-depth: 0
|
# 2. Node.js Tests — backend unit tests + Playwright E2E, coverage
|
||||||
|
# ───────────────────────────────────────────────────────────────
|
||||||
- name: Free disk space
|
node-test:
|
||||||
run: |
|
name: "🧪 Node.js Tests"
|
||||||
# Prune old runner diagnostic logs (can accumulate 50MB+)
|
runs-on: [self-hosted, Linux]
|
||||||
find ~/actions-runner/_diag/ -name '*.log' -mtime +3 -delete 2>/dev/null || true
|
steps:
|
||||||
# Show available disk space
|
- name: Checkout code
|
||||||
df -h / | tail -1
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
- name: Set up Node.js 22
|
fetch-depth: 2
|
||||||
uses: actions/setup-node@v5
|
|
||||||
with:
|
- name: Set up Node.js 22
|
||||||
node-version: '22'
|
uses: actions/setup-node@v5
|
||||||
|
with:
|
||||||
- name: Clean Go module cache
|
node-version: '22'
|
||||||
run: rm -rf ~/go/pkg/mod 2>/dev/null || true
|
|
||||||
|
- name: Install npm dependencies
|
||||||
- name: Set up Go 1.22
|
run: npm ci --production=false
|
||||||
uses: actions/setup-go@v6
|
|
||||||
with:
|
- name: Detect changed files
|
||||||
go-version: '1.22'
|
id: changes
|
||||||
cache-dependency-path: cmd/server/go.sum
|
run: |
|
||||||
|
BACKEND=$(git diff --name-only HEAD~1 | grep -cE '^(server|db|decoder|packet-store|server-helpers|iata-coords)\.js$' || true)
|
||||||
- name: Build Go server
|
FRONTEND=$(git diff --name-only HEAD~1 | grep -cE '^public/' || true)
|
||||||
run: |
|
TESTS=$(git diff --name-only HEAD~1 | grep -cE '^test-|^tools/' || true)
|
||||||
cd cmd/server
|
CI=$(git diff --name-only HEAD~1 | grep -cE '\.github/|package\.json|test-all\.sh|scripts/' || true)
|
||||||
go build -o ../../corescope-server .
|
# If CI/test infra changed, run everything
|
||||||
echo "Go server built successfully"
|
if [ "$CI" -gt 0 ]; then BACKEND=1; FRONTEND=1; fi
|
||||||
|
# If test files changed, run everything
|
||||||
- name: Install npm dependencies
|
if [ "$TESTS" -gt 0 ]; then BACKEND=1; FRONTEND=1; fi
|
||||||
run: npm ci --production=false
|
echo "backend=$([[ $BACKEND -gt 0 ]] && echo true || echo false)" >> $GITHUB_OUTPUT
|
||||||
|
echo "frontend=$([[ $FRONTEND -gt 0 ]] && echo true || echo false)" >> $GITHUB_OUTPUT
|
||||||
- name: Install Playwright browser
|
echo "Changes: backend=$BACKEND frontend=$FRONTEND tests=$TESTS ci=$CI"
|
||||||
run: |
|
|
||||||
npx playwright install chromium 2>/dev/null || true
|
- name: Run backend tests with coverage
|
||||||
npx playwright install-deps chromium 2>/dev/null || true
|
if: steps.changes.outputs.backend == 'true'
|
||||||
|
run: |
|
||||||
- name: Instrument frontend JS for coverage
|
npx c8 --reporter=text-summary --reporter=text sh test-all.sh 2>&1 | tee test-output.txt
|
||||||
run: sh scripts/instrument-frontend.sh
|
|
||||||
|
TOTAL_PASS=$(grep -oP '\d+(?= passed)' test-output.txt | awk '{s+=$1} END {print s}')
|
||||||
- name: Start Go server with fixture DB
|
TOTAL_FAIL=$(grep -oP '\d+(?= failed)' test-output.txt | awk '{s+=$1} END {print s}')
|
||||||
run: |
|
BE_COVERAGE=$(grep 'Statements' test-output.txt | tail -1 | grep -oP '[\d.]+(?=%)')
|
||||||
fuser -k 13581/tcp 2>/dev/null || true
|
|
||||||
sleep 1
|
mkdir -p .badges
|
||||||
./corescope-server -port 13581 -db test-fixtures/e2e-fixture.db -public public-instrumented &
|
BE_COLOR="red"
|
||||||
echo $! > .server.pid
|
[ "$(echo "$BE_COVERAGE > 60" | bc -l 2>/dev/null)" = "1" ] && BE_COLOR="yellow"
|
||||||
for i in $(seq 1 30); do
|
[ "$(echo "$BE_COVERAGE > 80" | bc -l 2>/dev/null)" = "1" ] && BE_COLOR="brightgreen"
|
||||||
if curl -sf http://localhost:13581/api/stats > /dev/null 2>&1; then
|
echo "{\"schemaVersion\":1,\"label\":\"backend tests\",\"message\":\"${TOTAL_PASS} passed\",\"color\":\"brightgreen\"}" > .badges/backend-tests.json
|
||||||
echo "Server ready after ${i}s"
|
echo "{\"schemaVersion\":1,\"label\":\"backend coverage\",\"message\":\"${BE_COVERAGE}%\",\"color\":\"${BE_COLOR}\"}" > .badges/backend-coverage.json
|
||||||
break
|
|
||||||
fi
|
echo "## Backend: ${TOTAL_PASS} tests, ${BE_COVERAGE}% coverage" >> $GITHUB_STEP_SUMMARY
|
||||||
if [ "$i" -eq 30 ]; then
|
|
||||||
echo "Server failed to start within 30s"
|
- name: Run backend tests (quick, no coverage)
|
||||||
exit 1
|
if: steps.changes.outputs.backend == 'false'
|
||||||
fi
|
run: npm run test:unit
|
||||||
sleep 1
|
|
||||||
done
|
- name: Install Playwright browser
|
||||||
|
if: steps.changes.outputs.frontend == 'true'
|
||||||
- name: Run Playwright E2E tests (fail-fast)
|
run: npx playwright install chromium --with-deps 2>/dev/null || true
|
||||||
run: |
|
|
||||||
BASE_URL=http://localhost:13581 node test-e2e-playwright.js 2>&1 | tee e2e-output.txt
|
- name: Instrument frontend JS for coverage
|
||||||
|
if: steps.changes.outputs.frontend == 'true'
|
||||||
- name: Collect frontend coverage (parallel)
|
run: sh scripts/instrument-frontend.sh
|
||||||
if: success() && github.event_name == 'push'
|
|
||||||
run: |
|
- name: Start instrumented test server on port 13581
|
||||||
BASE_URL=http://localhost:13581 node scripts/collect-frontend-coverage.js 2>&1 | tee fe-coverage-output.txt || true
|
if: steps.changes.outputs.frontend == 'true'
|
||||||
|
run: |
|
||||||
- name: Generate frontend coverage badges
|
# Kill any stale server on 13581
|
||||||
if: success()
|
fuser -k 13581/tcp 2>/dev/null || true
|
||||||
run: |
|
sleep 2
|
||||||
E2E_PASS=$(grep -oP '[0-9]+(?=/)' e2e-output.txt | tail -1 || echo "0")
|
COVERAGE=1 PORT=13581 node server.js &
|
||||||
|
echo $! > .server.pid
|
||||||
mkdir -p .badges
|
echo "Server PID: $(cat .server.pid)"
|
||||||
if [ -f .nyc_output/frontend-coverage.json ] || [ -f .nyc_output/e2e-coverage.json ]; then
|
# Health-check poll loop (up to 30s)
|
||||||
npx nyc report --reporter=text-summary --reporter=text 2>&1 | tee fe-report.txt
|
for i in $(seq 1 30); do
|
||||||
FE_COVERAGE=$(grep 'Statements' fe-report.txt | head -1 | grep -oP '[\d.]+(?=%)' || echo "0")
|
if curl -sf http://localhost:13581/api/stats > /dev/null 2>&1; then
|
||||||
FE_COVERAGE=${FE_COVERAGE:-0}
|
echo "Server ready after ${i}s"
|
||||||
FE_COLOR="red"
|
break
|
||||||
[ "$(echo "$FE_COVERAGE > 50" | bc -l 2>/dev/null)" = "1" ] && FE_COLOR="yellow"
|
fi
|
||||||
[ "$(echo "$FE_COVERAGE > 80" | bc -l 2>/dev/null)" = "1" ] && FE_COLOR="brightgreen"
|
if [ "$i" -eq 30 ]; then
|
||||||
echo "{\"schemaVersion\":1,\"label\":\"frontend coverage\",\"message\":\"${FE_COVERAGE}%\",\"color\":\"${FE_COLOR}\"}" > .badges/frontend-coverage.json
|
echo "Server failed to start within 30s"
|
||||||
echo "## Frontend: ${FE_COVERAGE}% coverage" >> $GITHUB_STEP_SUMMARY
|
echo "Last few lines from server logs:"
|
||||||
fi
|
ps aux | grep "PORT=13581" || echo "No server process found"
|
||||||
echo "{\"schemaVersion\":1,\"label\":\"e2e tests\",\"message\":\"${E2E_PASS:-0} passed\",\"color\":\"brightgreen\"}" > .badges/e2e-tests.json
|
exit 1
|
||||||
|
fi
|
||||||
- name: Stop test server
|
sleep 1
|
||||||
if: always()
|
done
|
||||||
run: |
|
|
||||||
if [ -f .server.pid ]; then
|
- name: Run Playwright E2E tests
|
||||||
kill $(cat .server.pid) 2>/dev/null || true
|
if: steps.changes.outputs.frontend == 'true'
|
||||||
rm -f .server.pid
|
run: BASE_URL=http://localhost:13581 node test-e2e-playwright.js 2>&1 | tee e2e-output.txt
|
||||||
fi
|
|
||||||
|
- name: Collect frontend coverage report
|
||||||
- name: Upload E2E badges
|
if: always() && steps.changes.outputs.frontend == 'true'
|
||||||
if: success()
|
run: |
|
||||||
uses: actions/upload-artifact@v6
|
BASE_URL=http://localhost:13581 node scripts/collect-frontend-coverage.js 2>&1 | tee fe-coverage-output.txt
|
||||||
with:
|
|
||||||
name: e2e-badges
|
E2E_PASS=$(grep -oP '[0-9]+(?=/)' e2e-output.txt | tail -1)
|
||||||
path: .badges/
|
|
||||||
retention-days: 1
|
mkdir -p .badges
|
||||||
if-no-files-found: ignore
|
if [ -f .nyc_output/frontend-coverage.json ]; then
|
||||||
include-hidden-files: true
|
npx nyc report --reporter=text-summary --reporter=text 2>&1 | tee fe-report.txt
|
||||||
|
FE_COVERAGE=$(grep 'Statements' fe-report.txt | head -1 | grep -oP '[\d.]+(?=%)' || echo "0")
|
||||||
# ───────────────────────────────────────────────────────────────
|
FE_COVERAGE=${FE_COVERAGE:-0}
|
||||||
# 3. Build & Publish Docker Image
|
FE_COLOR="red"
|
||||||
# ───────────────────────────────────────────────────────────────
|
[ "$(echo "$FE_COVERAGE > 50" | bc -l 2>/dev/null)" = "1" ] && FE_COLOR="yellow"
|
||||||
build-and-publish:
|
[ "$(echo "$FE_COVERAGE > 80" | bc -l 2>/dev/null)" = "1" ] && FE_COLOR="brightgreen"
|
||||||
name: "🏗️ Build & Publish Docker Image"
|
echo "{\"schemaVersion\":1,\"label\":\"frontend coverage\",\"message\":\"${FE_COVERAGE}%\",\"color\":\"${FE_COLOR}\"}" > .badges/frontend-coverage.json
|
||||||
needs: [e2e-test]
|
echo "## Frontend: ${FE_COVERAGE}% coverage" >> $GITHUB_STEP_SUMMARY
|
||||||
runs-on: [self-hosted, meshcore-runner-2]
|
fi
|
||||||
steps:
|
echo "{\"schemaVersion\":1,\"label\":\"frontend tests\",\"message\":\"${E2E_PASS:-0} E2E passed\",\"color\":\"brightgreen\"}" > .badges/frontend-tests.json
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v5
|
- name: Stop test server
|
||||||
|
if: always() && steps.changes.outputs.frontend == 'true'
|
||||||
- name: Free disk space
|
run: |
|
||||||
run: |
|
if [ -f .server.pid ]; then
|
||||||
docker system prune -af 2>/dev/null || true
|
kill $(cat .server.pid) 2>/dev/null || true
|
||||||
docker builder prune -af 2>/dev/null || true
|
rm -f .server.pid
|
||||||
df -h /
|
echo "Server stopped"
|
||||||
|
fi
|
||||||
- name: Compute build metadata
|
|
||||||
id: meta
|
- name: Run frontend E2E (quick, no coverage)
|
||||||
run: |
|
if: steps.changes.outputs.frontend == 'false'
|
||||||
BUILD_TIME=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
run: |
|
||||||
GIT_COMMIT="${GITHUB_SHA::7}"
|
fuser -k 13581/tcp 2>/dev/null || true
|
||||||
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
|
PORT=13581 node server.js &
|
||||||
APP_VERSION="${GITHUB_REF#refs/tags/}"
|
SERVER_PID=$!
|
||||||
else
|
sleep 5
|
||||||
APP_VERSION="edge"
|
BASE_URL=http://localhost:13581 node test-e2e-playwright.js || true
|
||||||
fi
|
kill $SERVER_PID 2>/dev/null || true
|
||||||
echo "build_time=$BUILD_TIME" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "git_commit=$GIT_COMMIT" >> "$GITHUB_OUTPUT"
|
- name: Cancel workflow on failure
|
||||||
echo "app_version=$APP_VERSION" >> "$GITHUB_OUTPUT"
|
if: failure()
|
||||||
echo "Build: version=$APP_VERSION commit=$GIT_COMMIT time=$BUILD_TIME"
|
run: |
|
||||||
|
curl -s -X POST \
|
||||||
- name: Build Go Docker image (local staging)
|
-H "Authorization: Bearer ${{ github.token }}" \
|
||||||
run: |
|
"https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/cancel"
|
||||||
GIT_COMMIT="${{ steps.meta.outputs.git_commit }}" \
|
|
||||||
APP_VERSION="${{ steps.meta.outputs.app_version }}" \
|
- name: Upload Node.js test badges
|
||||||
BUILD_TIME="${{ steps.meta.outputs.build_time }}" \
|
if: always()
|
||||||
docker compose -f "$STAGING_COMPOSE_FILE" -p corescope-staging build "$STAGING_SERVICE"
|
uses: actions/upload-artifact@v5
|
||||||
echo "Built Go staging image ✅"
|
with:
|
||||||
|
name: node-badges
|
||||||
- name: Set up Docker Buildx
|
path: .badges/
|
||||||
if: github.event_name == 'push'
|
retention-days: 1
|
||||||
uses: docker/setup-buildx-action@v3
|
if-no-files-found: ignore
|
||||||
|
|
||||||
- name: Log in to GHCR
|
# ───────────────────────────────────────────────────────────────
|
||||||
if: github.event_name == 'push'
|
# 3. Build Docker Image
|
||||||
uses: docker/login-action@v3
|
# ───────────────────────────────────────────────────────────────
|
||||||
with:
|
build:
|
||||||
registry: ghcr.io
|
name: "🏗️ Build Docker Image"
|
||||||
username: ${{ github.actor }}
|
if: github.event_name == 'push'
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
needs: [go-test, node-test]
|
||||||
|
runs-on: [self-hosted, Linux]
|
||||||
- name: Extract Docker metadata
|
steps:
|
||||||
if: github.event_name == 'push'
|
- name: Checkout code
|
||||||
id: docker-meta
|
uses: actions/checkout@v5
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
- name: Set up Node.js 22
|
||||||
images: ghcr.io/kpa-clawbot/corescope
|
uses: actions/setup-node@v5
|
||||||
tags: |
|
with:
|
||||||
type=semver,pattern=v{{version}}
|
node-version: '22'
|
||||||
type=semver,pattern=v{{major}}.{{minor}}
|
|
||||||
type=semver,pattern=v{{major}}
|
- name: Build Go Docker image
|
||||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
run: |
|
||||||
type=edge,branch=master
|
echo "${GITHUB_SHA::7}" > .git-commit
|
||||||
|
APP_VERSION=$(node -p "require('./package.json').version") \
|
||||||
- name: Build and push to GHCR
|
GIT_COMMIT="${GITHUB_SHA::7}" \
|
||||||
if: github.event_name == 'push'
|
docker compose --profile staging-go build staging-go
|
||||||
uses: docker/build-push-action@v6
|
echo "Built Go staging image"
|
||||||
with:
|
|
||||||
context: .
|
# ───────────────────────────────────────────────────────────────
|
||||||
push: true
|
# 4. Deploy Staging — start on port 82, healthcheck, smoke test
|
||||||
platforms: linux/amd64
|
# ───────────────────────────────────────────────────────────────
|
||||||
tags: ${{ steps.docker-meta.outputs.tags }}
|
deploy:
|
||||||
labels: ${{ steps.docker-meta.outputs.labels }}
|
name: "🚀 Deploy Staging"
|
||||||
build-args: |
|
if: github.event_name == 'push'
|
||||||
APP_VERSION=${{ steps.meta.outputs.app_version }}
|
needs: [build]
|
||||||
GIT_COMMIT=${{ steps.meta.outputs.git_commit }}
|
runs-on: [self-hosted, Linux]
|
||||||
BUILD_TIME=${{ steps.meta.outputs.build_time }}
|
steps:
|
||||||
cache-from: type=gha
|
- name: Checkout code
|
||||||
cache-to: type=gha,mode=max
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
# ───────────────────────────────────────────────────────────────
|
- name: Start staging on port 82
|
||||||
# 4. Release Artifacts (tags only)
|
run: |
|
||||||
# ───────────────────────────────────────────────────────────────
|
# Force remove stale containers
|
||||||
release-artifacts:
|
docker rm -f corescope-staging-go 2>/dev/null || true
|
||||||
name: "📦 Release Artifacts"
|
# Clean up stale ports
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
fuser -k 82/tcp 2>/dev/null || true
|
||||||
needs: [go-test]
|
docker compose --profile staging-go up -d staging-go
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
- name: Healthcheck staging container
|
||||||
contents: write
|
run: |
|
||||||
steps:
|
for i in $(seq 1 120); do
|
||||||
- name: Checkout code
|
HEALTH=$(docker inspect corescope-staging-go --format '{{.State.Health.Status}}' 2>/dev/null || echo "starting")
|
||||||
uses: actions/checkout@v5
|
if [ "$HEALTH" = "healthy" ]; then
|
||||||
|
echo "Staging healthy after ${i}s"
|
||||||
- name: Set up Go 1.22
|
break
|
||||||
uses: actions/setup-go@v6
|
fi
|
||||||
with:
|
if [ "$i" -eq 120 ]; then
|
||||||
go-version: '1.22'
|
echo "Staging failed health check after 120s"
|
||||||
|
docker logs corescope-staging-go --tail 50
|
||||||
- name: Build corescope-decrypt (static, linux/amd64)
|
exit 1
|
||||||
run: |
|
fi
|
||||||
cd cmd/decrypt
|
sleep 1
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=${{ github.ref_name }}" -o ../../corescope-decrypt-linux-amd64 .
|
done
|
||||||
|
|
||||||
- name: Build corescope-decrypt (static, linux/arm64)
|
- name: Smoke test staging API
|
||||||
run: |
|
run: |
|
||||||
cd cmd/decrypt
|
if curl -sf http://localhost:82/api/stats | grep -q engine; then
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.version=${{ github.ref_name }}" -o ../../corescope-decrypt-linux-arm64 .
|
echo "Staging verified — engine field present ✅"
|
||||||
|
else
|
||||||
- name: Upload release assets
|
echo "Staging /api/stats did not return engine field"
|
||||||
uses: softprops/action-gh-release@v2
|
exit 1
|
||||||
with:
|
fi
|
||||||
files: |
|
|
||||||
corescope-decrypt-linux-amd64
|
# ───────────────────────────────────────────────────────────────
|
||||||
corescope-decrypt-linux-arm64
|
# 5. Publish Badges & Summary
|
||||||
|
# ───────────────────────────────────────────────────────────────
|
||||||
# ───────────────────────────────────────────────────────────────
|
publish:
|
||||||
# 4b. Deploy Staging (master only)
|
name: "📝 Publish Badges & Summary"
|
||||||
# ───────────────────────────────────────────────────────────────
|
if: github.event_name == 'push'
|
||||||
deploy:
|
needs: [deploy]
|
||||||
name: "🚀 Deploy Staging"
|
runs-on: [self-hosted, Linux]
|
||||||
if: github.event_name == 'push'
|
steps:
|
||||||
needs: [build-and-publish]
|
- name: Checkout code
|
||||||
runs-on: [self-hosted, meshcore-runner-2]
|
uses: actions/checkout@v5
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
- name: Download Go coverage badges
|
||||||
uses: actions/checkout@v5
|
continue-on-error: true
|
||||||
|
uses: actions/download-artifact@v5
|
||||||
- name: Pull latest image from GHCR
|
with:
|
||||||
run: |
|
name: go-badges
|
||||||
# Try to pull the edge image from GHCR and tag for docker-compose compatibility
|
path: .badges/
|
||||||
if docker pull ghcr.io/kpa-clawbot/corescope:edge; then
|
|
||||||
docker tag ghcr.io/kpa-clawbot/corescope:edge corescope-go:latest
|
- name: Download Node.js test badges
|
||||||
echo "Pulled and tagged GHCR edge image ✅"
|
continue-on-error: true
|
||||||
else
|
uses: actions/download-artifact@v5
|
||||||
echo "⚠️ GHCR pull failed — falling back to locally built image"
|
with:
|
||||||
fi
|
name: node-badges
|
||||||
|
path: .badges/
|
||||||
- name: Deploy staging
|
|
||||||
run: |
|
- name: Publish coverage badges to repo
|
||||||
# Force-remove the staging container regardless of how it was created
|
continue-on-error: true
|
||||||
# (compose-managed OR manually created via docker run)
|
run: |
|
||||||
docker stop corescope-staging-go 2>/dev/null || true
|
git config user.name "github-actions"
|
||||||
docker rm -f corescope-staging-go 2>/dev/null || true
|
git config user.email "actions@github.com"
|
||||||
docker compose -f "$STAGING_COMPOSE_FILE" -p corescope-staging down --timeout 30 2>/dev/null || true
|
git remote set-url origin https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git
|
||||||
|
git add .badges/ -f
|
||||||
# Wait for container to be fully gone and OS to reclaim memory (3GB limit)
|
git diff --cached --quiet || (git commit -m "ci: update test badges [skip ci]" && git push) || echo "Badge push failed"
|
||||||
for i in $(seq 1 15); do
|
|
||||||
if ! docker ps -a --format '{{.Names}}' | grep -q 'corescope-staging-go'; then
|
- name: Post deployment summary
|
||||||
break
|
run: |
|
||||||
fi
|
echo "## Staging Deployed ✓" >> $GITHUB_STEP_SUMMARY
|
||||||
sleep 1
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
done
|
echo "**Commit:** \`$(git rev-parse --short HEAD)\` — $(git log -1 --format=%s)" >> $GITHUB_STEP_SUMMARY
|
||||||
sleep 5 # extra pause for OS memory reclaim
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Staging:** http://<VM_HOST>:82" >> $GITHUB_STEP_SUMMARY
|
||||||
# Ensure staging data dir exists (config.json lives here, no separate file mount)
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
STAGING_DATA="${STAGING_DATA_DIR:-$HOME/meshcore-staging-data}"
|
echo "To promote to production:" >> $GITHUB_STEP_SUMMARY
|
||||||
mkdir -p "$STAGING_DATA"
|
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "ssh deploy@\$VM_HOST" >> $GITHUB_STEP_SUMMARY
|
||||||
# If no config exists, copy the example (CI doesn't have a real prod config)
|
echo "cd /opt/corescope-deploy" >> $GITHUB_STEP_SUMMARY
|
||||||
if [ ! -f "$STAGING_DATA/config.json" ]; then
|
echo "./manage.sh promote" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Staging config missing — copying config.example.json"
|
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||||
cp config.example.json "$STAGING_DATA/config.json" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker compose -f "$STAGING_COMPOSE_FILE" -p corescope-staging up -d staging-go
|
|
||||||
|
|
||||||
- name: Healthcheck staging container
|
|
||||||
run: |
|
|
||||||
for i in $(seq 1 120); do
|
|
||||||
HEALTH=$(docker inspect corescope-staging-go --format '{{.State.Health.Status}}' 2>/dev/null || echo "starting")
|
|
||||||
if [ "$HEALTH" = "healthy" ]; then
|
|
||||||
echo "Staging healthy after ${i}s"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [ "$i" -eq 120 ]; then
|
|
||||||
echo "Staging failed health check after 120s"
|
|
||||||
docker logs corescope-staging-go --tail 50
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Smoke test staging API
|
|
||||||
run: |
|
|
||||||
if curl -sf http://localhost:82/api/stats | grep -q engine; then
|
|
||||||
echo "Staging verified — engine field present ✅"
|
|
||||||
else
|
|
||||||
echo "Staging /api/stats did not return engine field"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Clean up old Docker images
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
# Remove dangling images and images older than 24h (keeps current build)
|
|
||||||
echo "--- Docker disk usage before cleanup ---"
|
|
||||||
docker system df
|
|
||||||
docker image prune -af --filter "until=24h" 2>/dev/null || true
|
|
||||||
docker builder prune -f --keep-storage=1GB 2>/dev/null || true
|
|
||||||
echo "--- Docker disk usage after cleanup ---"
|
|
||||||
docker system df
|
|
||||||
|
|
||||||
# ───────────────────────────────────────────────────────────────
|
|
||||||
# 5. Publish Badges & Summary (master only)
|
|
||||||
# ───────────────────────────────────────────────────────────────
|
|
||||||
publish:
|
|
||||||
name: "📝 Publish Badges & Summary"
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
needs: [deploy]
|
|
||||||
runs-on: [self-hosted, Linux]
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Download Go coverage badges
|
|
||||||
continue-on-error: true
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: go-badges
|
|
||||||
path: .badges/
|
|
||||||
|
|
||||||
- name: Download E2E badges
|
|
||||||
continue-on-error: true
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: e2e-badges
|
|
||||||
path: .badges/
|
|
||||||
|
|
||||||
- name: Publish coverage badges to repo
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.BADGE_PUSH_TOKEN }}
|
|
||||||
run: |
|
|
||||||
# GITHUB_TOKEN cannot push to protected branches (required status checks).
|
|
||||||
# Use admin PAT (BADGE_PUSH_TOKEN) via GitHub Contents API instead.
|
|
||||||
for badge in .badges/*.json; do
|
|
||||||
FILENAME=$(basename "$badge")
|
|
||||||
FILEPATH=".badges/$FILENAME"
|
|
||||||
CONTENT=$(base64 -w0 "$badge")
|
|
||||||
CURRENT_SHA=$(gh api "repos/${{ github.repository }}/contents/$FILEPATH" --jq '.sha' 2>/dev/null || echo "")
|
|
||||||
if [ -n "$CURRENT_SHA" ]; then
|
|
||||||
gh api "repos/${{ github.repository }}/contents/$FILEPATH" \
|
|
||||||
-X PUT \
|
|
||||||
-f message="ci: update $FILENAME [skip ci]" \
|
|
||||||
-f content="$CONTENT" \
|
|
||||||
-f sha="$CURRENT_SHA" \
|
|
||||||
-f branch="master" \
|
|
||||||
--silent 2>&1 || echo "Failed to update $FILENAME"
|
|
||||||
else
|
|
||||||
gh api "repos/${{ github.repository }}/contents/$FILEPATH" \
|
|
||||||
-X PUT \
|
|
||||||
-f message="ci: update $FILENAME [skip ci]" \
|
|
||||||
-f content="$CONTENT" \
|
|
||||||
-f branch="master" \
|
|
||||||
--silent 2>&1 || echo "Failed to create $FILENAME"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "Badge publish complete"
|
|
||||||
|
|
||||||
- name: Post deployment summary
|
|
||||||
run: |
|
|
||||||
echo "## Staging Deployed ✓" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "**Commit:** \`$(git rev-parse --short HEAD)\` — $(git log -1 --format=%s)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|||||||
171
.github/workflows/squad-heartbeat.yml
vendored
171
.github/workflows/squad-heartbeat.yml
vendored
@@ -1,171 +0,0 @@
|
|||||||
name: Squad Heartbeat (Ralph)
|
|
||||||
# ⚠️ SYNC: This workflow is maintained in 4 locations. Changes must be applied to all:
|
|
||||||
# - templates/workflows/squad-heartbeat.yml (source template)
|
|
||||||
# - packages/squad-cli/templates/workflows/squad-heartbeat.yml (CLI package)
|
|
||||||
# - .squad/templates/workflows/squad-heartbeat.yml (installed template)
|
|
||||||
# - .github/workflows/squad-heartbeat.yml (active workflow)
|
|
||||||
# Run 'squad upgrade' to sync installed copies from source templates.
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Every 30 minutes — adjust via cron expression as needed
|
|
||||||
- cron: '*/30 * * * *'
|
|
||||||
|
|
||||||
# React to completed work or new squad work
|
|
||||||
issues:
|
|
||||||
types: [closed, labeled]
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
|
|
||||||
# Manual trigger
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
heartbeat:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Check triage script
|
|
||||||
id: check-script
|
|
||||||
run: |
|
|
||||||
if [ -f ".squad/templates/ralph-triage.js" ]; then
|
|
||||||
echo "has_script=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "has_script=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "⚠️ ralph-triage.js not found — run 'squad upgrade' to install"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Ralph — Smart triage
|
|
||||||
if: steps.check-script.outputs.has_script == 'true'
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
node .squad/templates/ralph-triage.js \
|
|
||||||
--squad-dir .squad \
|
|
||||||
--output triage-results.json
|
|
||||||
|
|
||||||
- name: Ralph — Apply triage decisions
|
|
||||||
if: steps.check-script.outputs.has_script == 'true' && hashFiles('triage-results.json') != ''
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = 'triage-results.json';
|
|
||||||
if (!fs.existsSync(path)) {
|
|
||||||
core.info('No triage results — board is clear');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
||||||
if (results.length === 0) {
|
|
||||||
core.info('📋 Board is clear — Ralph found no untriaged issues');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const decision of results) {
|
|
||||||
try {
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: decision.issueNumber,
|
|
||||||
labels: [decision.label]
|
|
||||||
});
|
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: decision.issueNumber,
|
|
||||||
body: [
|
|
||||||
'### 🔄 Ralph — Auto-Triage',
|
|
||||||
'',
|
|
||||||
`**Assigned to:** ${decision.assignTo}`,
|
|
||||||
`**Reason:** ${decision.reason}`,
|
|
||||||
`**Source:** ${decision.source}`,
|
|
||||||
'',
|
|
||||||
'> Ralph auto-triaged this issue using routing rules.',
|
|
||||||
'> To reassign, swap the `squad:*` label.'
|
|
||||||
].join('\n')
|
|
||||||
});
|
|
||||||
|
|
||||||
core.info(`Triaged #${decision.issueNumber} → ${decision.assignTo} (${decision.source})`);
|
|
||||||
} catch (e) {
|
|
||||||
core.warning(`Failed to triage #${decision.issueNumber}: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info(`🔄 Ralph triaged ${results.length} issue(s)`);
|
|
||||||
|
|
||||||
# Copilot auto-assign step (uses PAT if available)
|
|
||||||
- name: Ralph — Assign @copilot issues
|
|
||||||
if: success()
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN || secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
let teamFile = '.squad/team.md';
|
|
||||||
if (!fs.existsSync(teamFile)) {
|
|
||||||
teamFile = '.ai-team/team.md';
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(teamFile)) return;
|
|
||||||
|
|
||||||
const content = fs.readFileSync(teamFile, 'utf8');
|
|
||||||
|
|
||||||
// Check if @copilot is on the team with auto-assign
|
|
||||||
const hasCopilot = content.includes('🤖 Coding Agent') || content.includes('@copilot');
|
|
||||||
const autoAssign = content.includes('<!-- copilot-auto-assign: true -->');
|
|
||||||
if (!hasCopilot || !autoAssign) return;
|
|
||||||
|
|
||||||
// Find issues labeled squad:copilot with no assignee
|
|
||||||
try {
|
|
||||||
const { data: copilotIssues } = await github.rest.issues.listForRepo({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
labels: 'squad:copilot',
|
|
||||||
state: 'open',
|
|
||||||
per_page: 5
|
|
||||||
});
|
|
||||||
|
|
||||||
const unassigned = copilotIssues.filter(i =>
|
|
||||||
!i.assignees || i.assignees.length === 0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (unassigned.length === 0) {
|
|
||||||
core.info('No unassigned squad:copilot issues');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get repo default branch
|
|
||||||
const { data: repoData } = await github.rest.repos.get({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const issue of unassigned) {
|
|
||||||
try {
|
|
||||||
await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', {
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
assignees: ['copilot-swe-agent[bot]'],
|
|
||||||
agent_assignment: {
|
|
||||||
target_repo: `${context.repo.owner}/${context.repo.repo}`,
|
|
||||||
base_branch: repoData.default_branch,
|
|
||||||
custom_instructions: `Read .squad/team.md (or .ai-team/team.md) for team context and .squad/routing.md (or .ai-team/routing.md) for routing rules.`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
core.info(`Assigned copilot-swe-agent[bot] to #${issue.number}`);
|
|
||||||
} catch (e) {
|
|
||||||
core.warning(`Failed to assign @copilot to #${issue.number}: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
core.info(`No squad:copilot label found or error: ${e.message}`);
|
|
||||||
}
|
|
||||||
161
.github/workflows/squad-issue-assign.yml
vendored
161
.github/workflows/squad-issue-assign.yml
vendored
@@ -1,161 +0,0 @@
|
|||||||
name: Squad Issue Assign
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [labeled]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
assign-work:
|
|
||||||
# Only trigger on squad:{member} labels (not the base "squad" label)
|
|
||||||
if: startsWith(github.event.label.name, 'squad:')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Identify assigned member and trigger work
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const fs = require('fs');
|
|
||||||
const issue = context.payload.issue;
|
|
||||||
const label = context.payload.label.name;
|
|
||||||
|
|
||||||
// Extract member name from label (e.g., "squad:ripley" → "ripley")
|
|
||||||
const memberName = label.replace('squad:', '').toLowerCase();
|
|
||||||
|
|
||||||
// Read team roster — check .squad/ first, fall back to .ai-team/
|
|
||||||
let teamFile = '.squad/team.md';
|
|
||||||
if (!fs.existsSync(teamFile)) {
|
|
||||||
teamFile = '.ai-team/team.md';
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(teamFile)) {
|
|
||||||
core.warning('No .squad/team.md or .ai-team/team.md found — cannot assign work');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = fs.readFileSync(teamFile, 'utf8');
|
|
||||||
const lines = content.split('\n');
|
|
||||||
|
|
||||||
// Check if this is a coding agent assignment
|
|
||||||
const isCopilotAssignment = memberName === 'copilot';
|
|
||||||
|
|
||||||
let assignedMember = null;
|
|
||||||
if (isCopilotAssignment) {
|
|
||||||
assignedMember = { name: '@copilot', role: 'Coding Agent' };
|
|
||||||
} else {
|
|
||||||
let inMembersTable = false;
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.match(/^##\s+(Members|Team Roster)/i)) {
|
|
||||||
inMembersTable = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (inMembersTable && line.startsWith('## ')) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) {
|
|
||||||
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
||||||
if (cells.length >= 2 && cells[0].toLowerCase() === memberName) {
|
|
||||||
assignedMember = { name: cells[0], role: cells[1] };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!assignedMember) {
|
|
||||||
core.warning(`No member found matching label "${label}"`);
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
body: `⚠️ No squad member found matching label \`${label}\`. Check \`.squad/team.md\` (or \`.ai-team/team.md\`) for valid member names.`
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post assignment acknowledgment
|
|
||||||
let comment;
|
|
||||||
if (isCopilotAssignment) {
|
|
||||||
comment = [
|
|
||||||
`### 🤖 Routed to @copilot (Coding Agent)`,
|
|
||||||
'',
|
|
||||||
`**Issue:** #${issue.number} — ${issue.title}`,
|
|
||||||
'',
|
|
||||||
`@copilot has been assigned and will pick this up automatically.`,
|
|
||||||
'',
|
|
||||||
`> The coding agent will create a \`copilot/*\` branch and open a draft PR.`,
|
|
||||||
`> Review the PR as you would any team member's work.`,
|
|
||||||
].join('\n');
|
|
||||||
} else {
|
|
||||||
comment = [
|
|
||||||
`### 📋 Assigned to ${assignedMember.name} (${assignedMember.role})`,
|
|
||||||
'',
|
|
||||||
`**Issue:** #${issue.number} — ${issue.title}`,
|
|
||||||
'',
|
|
||||||
`${assignedMember.name} will pick this up in the next Copilot session.`,
|
|
||||||
'',
|
|
||||||
`> **For Copilot coding agent:** If enabled, this issue will be worked automatically.`,
|
|
||||||
`> Otherwise, start a Copilot session and say:`,
|
|
||||||
`> \`${assignedMember.name}, work on issue #${issue.number}\``,
|
|
||||||
].join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
body: comment
|
|
||||||
});
|
|
||||||
|
|
||||||
core.info(`Issue #${issue.number} assigned to ${assignedMember.name} (${assignedMember.role})`);
|
|
||||||
|
|
||||||
# Separate step: assign @copilot using PAT (required for coding agent)
|
|
||||||
- name: Assign @copilot coding agent
|
|
||||||
if: github.event.label.name == 'squad:copilot'
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const owner = context.repo.owner;
|
|
||||||
const repo = context.repo.repo;
|
|
||||||
const issue_number = context.payload.issue.number;
|
|
||||||
|
|
||||||
// Get the default branch name (main, master, etc.)
|
|
||||||
const { data: repoData } = await github.rest.repos.get({ owner, repo });
|
|
||||||
const baseBranch = repoData.default_branch;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', {
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number,
|
|
||||||
assignees: ['copilot-swe-agent[bot]'],
|
|
||||||
agent_assignment: {
|
|
||||||
target_repo: `${owner}/${repo}`,
|
|
||||||
base_branch: baseBranch,
|
|
||||||
custom_instructions: '',
|
|
||||||
custom_agent: '',
|
|
||||||
model: ''
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
'X-GitHub-Api-Version': '2022-11-28'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
core.info(`Assigned copilot-swe-agent to issue #${issue_number} (base: ${baseBranch})`);
|
|
||||||
} catch (err) {
|
|
||||||
core.warning(`Assignment with agent_assignment failed: ${err.message}`);
|
|
||||||
// Fallback: try without agent_assignment
|
|
||||||
try {
|
|
||||||
await github.rest.issues.addAssignees({
|
|
||||||
owner, repo, issue_number,
|
|
||||||
assignees: ['copilot-swe-agent']
|
|
||||||
});
|
|
||||||
core.info(`Fallback assigned copilot-swe-agent to issue #${issue_number}`);
|
|
||||||
} catch (err2) {
|
|
||||||
core.warning(`Fallback also failed: ${err2.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
260
.github/workflows/squad-triage.yml
vendored
260
.github/workflows/squad-triage.yml
vendored
@@ -1,260 +0,0 @@
|
|||||||
name: Squad Triage
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [labeled]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
triage:
|
|
||||||
if: github.event.label.name == 'squad'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Triage issue via Lead agent
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const fs = require('fs');
|
|
||||||
const issue = context.payload.issue;
|
|
||||||
|
|
||||||
// Read team roster — check .squad/ first, fall back to .ai-team/
|
|
||||||
let teamFile = '.squad/team.md';
|
|
||||||
if (!fs.existsSync(teamFile)) {
|
|
||||||
teamFile = '.ai-team/team.md';
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(teamFile)) {
|
|
||||||
core.warning('No .squad/team.md or .ai-team/team.md found — cannot triage');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = fs.readFileSync(teamFile, 'utf8');
|
|
||||||
const lines = content.split('\n');
|
|
||||||
|
|
||||||
// Check if @copilot is on the team
|
|
||||||
const hasCopilot = content.includes('🤖 Coding Agent');
|
|
||||||
const copilotAutoAssign = content.includes('<!-- copilot-auto-assign: true -->');
|
|
||||||
|
|
||||||
// Parse @copilot capability profile
|
|
||||||
let goodFitKeywords = [];
|
|
||||||
let needsReviewKeywords = [];
|
|
||||||
let notSuitableKeywords = [];
|
|
||||||
|
|
||||||
if (hasCopilot) {
|
|
||||||
// Extract capability tiers from team.md
|
|
||||||
const goodFitMatch = content.match(/🟢\s*Good fit[^:]*:\s*(.+)/i);
|
|
||||||
const needsReviewMatch = content.match(/🟡\s*Needs review[^:]*:\s*(.+)/i);
|
|
||||||
const notSuitableMatch = content.match(/🔴\s*Not suitable[^:]*:\s*(.+)/i);
|
|
||||||
|
|
||||||
if (goodFitMatch) {
|
|
||||||
goodFitKeywords = goodFitMatch[1].toLowerCase().split(',').map(s => s.trim());
|
|
||||||
} else {
|
|
||||||
goodFitKeywords = ['bug fix', 'test coverage', 'lint', 'format', 'dependency update', 'small feature', 'scaffolding', 'doc fix', 'documentation'];
|
|
||||||
}
|
|
||||||
if (needsReviewMatch) {
|
|
||||||
needsReviewKeywords = needsReviewMatch[1].toLowerCase().split(',').map(s => s.trim());
|
|
||||||
} else {
|
|
||||||
needsReviewKeywords = ['medium feature', 'refactoring', 'api endpoint', 'migration'];
|
|
||||||
}
|
|
||||||
if (notSuitableMatch) {
|
|
||||||
notSuitableKeywords = notSuitableMatch[1].toLowerCase().split(',').map(s => s.trim());
|
|
||||||
} else {
|
|
||||||
notSuitableKeywords = ['architecture', 'system design', 'security', 'auth', 'encryption', 'performance'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const members = [];
|
|
||||||
let inMembersTable = false;
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.match(/^##\s+(Members|Team Roster)/i)) {
|
|
||||||
inMembersTable = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (inMembersTable && line.startsWith('## ')) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) {
|
|
||||||
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
||||||
if (cells.length >= 2 && cells[0] !== 'Scribe') {
|
|
||||||
members.push({
|
|
||||||
name: cells[0],
|
|
||||||
role: cells[1]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read routing rules — check .squad/ first, fall back to .ai-team/
|
|
||||||
let routingFile = '.squad/routing.md';
|
|
||||||
if (!fs.existsSync(routingFile)) {
|
|
||||||
routingFile = '.ai-team/routing.md';
|
|
||||||
}
|
|
||||||
let routingContent = '';
|
|
||||||
if (fs.existsSync(routingFile)) {
|
|
||||||
routingContent = fs.readFileSync(routingFile, 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the Lead
|
|
||||||
const lead = members.find(m =>
|
|
||||||
m.role.toLowerCase().includes('lead') ||
|
|
||||||
m.role.toLowerCase().includes('architect') ||
|
|
||||||
m.role.toLowerCase().includes('coordinator')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!lead) {
|
|
||||||
core.warning('No Lead role found in team roster — cannot triage');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build triage context
|
|
||||||
const memberList = members.map(m =>
|
|
||||||
`- **${m.name}** (${m.role}) → label: \`squad:${m.name.toLowerCase()}\``
|
|
||||||
).join('\n');
|
|
||||||
|
|
||||||
// Determine best assignee based on issue content and routing
|
|
||||||
const issueText = `${issue.title}\n${issue.body || ''}`.toLowerCase();
|
|
||||||
|
|
||||||
let assignedMember = null;
|
|
||||||
let triageReason = '';
|
|
||||||
let copilotTier = null;
|
|
||||||
|
|
||||||
// First, evaluate @copilot fit if enabled
|
|
||||||
if (hasCopilot) {
|
|
||||||
const isNotSuitable = notSuitableKeywords.some(kw => issueText.includes(kw));
|
|
||||||
const isGoodFit = !isNotSuitable && goodFitKeywords.some(kw => issueText.includes(kw));
|
|
||||||
const isNeedsReview = !isNotSuitable && !isGoodFit && needsReviewKeywords.some(kw => issueText.includes(kw));
|
|
||||||
|
|
||||||
if (isGoodFit) {
|
|
||||||
copilotTier = 'good-fit';
|
|
||||||
assignedMember = { name: '@copilot', role: 'Coding Agent' };
|
|
||||||
triageReason = '🟢 Good fit for @copilot — matches capability profile';
|
|
||||||
} else if (isNeedsReview) {
|
|
||||||
copilotTier = 'needs-review';
|
|
||||||
assignedMember = { name: '@copilot', role: 'Coding Agent' };
|
|
||||||
triageReason = '🟡 Routing to @copilot (needs review) — a squad member should review the PR';
|
|
||||||
} else if (isNotSuitable) {
|
|
||||||
copilotTier = 'not-suitable';
|
|
||||||
// Fall through to normal routing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not routed to @copilot, use keyword-based routing
|
|
||||||
if (!assignedMember) {
|
|
||||||
for (const member of members) {
|
|
||||||
const role = member.role.toLowerCase();
|
|
||||||
if ((role.includes('frontend') || role.includes('ui')) &&
|
|
||||||
(issueText.includes('ui') || issueText.includes('frontend') ||
|
|
||||||
issueText.includes('css') || issueText.includes('component') ||
|
|
||||||
issueText.includes('button') || issueText.includes('page') ||
|
|
||||||
issueText.includes('layout') || issueText.includes('design'))) {
|
|
||||||
assignedMember = member;
|
|
||||||
triageReason = 'Issue relates to frontend/UI work';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if ((role.includes('backend') || role.includes('api') || role.includes('server')) &&
|
|
||||||
(issueText.includes('api') || issueText.includes('backend') ||
|
|
||||||
issueText.includes('database') || issueText.includes('endpoint') ||
|
|
||||||
issueText.includes('server') || issueText.includes('auth'))) {
|
|
||||||
assignedMember = member;
|
|
||||||
triageReason = 'Issue relates to backend/API work';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if ((role.includes('test') || role.includes('qa') || role.includes('quality')) &&
|
|
||||||
(issueText.includes('test') || issueText.includes('bug') ||
|
|
||||||
issueText.includes('fix') || issueText.includes('regression') ||
|
|
||||||
issueText.includes('coverage'))) {
|
|
||||||
assignedMember = member;
|
|
||||||
triageReason = 'Issue relates to testing/quality work';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if ((role.includes('devops') || role.includes('infra') || role.includes('ops')) &&
|
|
||||||
(issueText.includes('deploy') || issueText.includes('ci') ||
|
|
||||||
issueText.includes('pipeline') || issueText.includes('docker') ||
|
|
||||||
issueText.includes('infrastructure'))) {
|
|
||||||
assignedMember = member;
|
|
||||||
triageReason = 'Issue relates to DevOps/infrastructure work';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to Lead if no routing match
|
|
||||||
if (!assignedMember) {
|
|
||||||
assignedMember = lead;
|
|
||||||
triageReason = 'No specific domain match — assigned to Lead for further analysis';
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCopilot = assignedMember.name === '@copilot';
|
|
||||||
const assignLabel = isCopilot ? 'squad:copilot' : `squad:${assignedMember.name.toLowerCase()}`;
|
|
||||||
|
|
||||||
// Add the member-specific label
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
labels: [assignLabel]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply default triage verdict
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
labels: ['go:needs-research']
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-assign @copilot if enabled
|
|
||||||
if (isCopilot && copilotAutoAssign) {
|
|
||||||
try {
|
|
||||||
await github.rest.issues.addAssignees({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
assignees: ['copilot']
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
core.warning(`Could not auto-assign @copilot: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build copilot evaluation note
|
|
||||||
let copilotNote = '';
|
|
||||||
if (hasCopilot && !isCopilot) {
|
|
||||||
if (copilotTier === 'not-suitable') {
|
|
||||||
copilotNote = `\n\n**@copilot evaluation:** 🔴 Not suitable — issue involves work outside the coding agent's capability profile.`;
|
|
||||||
} else {
|
|
||||||
copilotNote = `\n\n**@copilot evaluation:** No strong capability match — routed to squad member.`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post triage comment
|
|
||||||
const comment = [
|
|
||||||
`### 🏗️ Squad Triage — ${lead.name} (${lead.role})`,
|
|
||||||
'',
|
|
||||||
`**Issue:** #${issue.number} — ${issue.title}`,
|
|
||||||
`**Assigned to:** ${assignedMember.name} (${assignedMember.role})`,
|
|
||||||
`**Reason:** ${triageReason}`,
|
|
||||||
copilotTier === 'needs-review' ? `\n⚠️ **PR review recommended** — a squad member should review @copilot's work on this one.` : '',
|
|
||||||
copilotNote,
|
|
||||||
'',
|
|
||||||
`---`,
|
|
||||||
'',
|
|
||||||
`**Team roster:**`,
|
|
||||||
memberList,
|
|
||||||
hasCopilot ? `- **@copilot** (Coding Agent) → label: \`squad:copilot\`` : '',
|
|
||||||
'',
|
|
||||||
`> To reassign, remove the current \`squad:*\` label and add the correct one.`,
|
|
||||||
].filter(Boolean).join('\n');
|
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
body: comment
|
|
||||||
});
|
|
||||||
|
|
||||||
core.info(`Triaged issue #${issue.number} → ${assignedMember.name} (${assignLabel})`);
|
|
||||||
169
.github/workflows/sync-squad-labels.yml
vendored
169
.github/workflows/sync-squad-labels.yml
vendored
@@ -1,169 +0,0 @@
|
|||||||
name: Sync Squad Labels
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- '.squad/team.md'
|
|
||||||
- '.ai-team/team.md'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync-labels:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Parse roster and sync labels
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const fs = require('fs');
|
|
||||||
let teamFile = '.squad/team.md';
|
|
||||||
if (!fs.existsSync(teamFile)) {
|
|
||||||
teamFile = '.ai-team/team.md';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(teamFile)) {
|
|
||||||
core.info('No .squad/team.md or .ai-team/team.md found — skipping label sync');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = fs.readFileSync(teamFile, 'utf8');
|
|
||||||
const lines = content.split('\n');
|
|
||||||
|
|
||||||
// Parse the Members table for agent names
|
|
||||||
const members = [];
|
|
||||||
let inMembersTable = false;
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.match(/^##\s+(Members|Team Roster)/i)) {
|
|
||||||
inMembersTable = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (inMembersTable && line.startsWith('## ')) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) {
|
|
||||||
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
||||||
if (cells.length >= 2 && cells[0] !== 'Scribe') {
|
|
||||||
members.push({
|
|
||||||
name: cells[0],
|
|
||||||
role: cells[1]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info(`Found ${members.length} squad members: ${members.map(m => m.name).join(', ')}`);
|
|
||||||
|
|
||||||
// Check if @copilot is on the team
|
|
||||||
const hasCopilot = content.includes('🤖 Coding Agent');
|
|
||||||
|
|
||||||
// Define label color palette for squad labels
|
|
||||||
const SQUAD_COLOR = '9B8FCC';
|
|
||||||
const MEMBER_COLOR = '9B8FCC';
|
|
||||||
const COPILOT_COLOR = '10b981';
|
|
||||||
|
|
||||||
// Define go: and release: labels (static)
|
|
||||||
const GO_LABELS = [
|
|
||||||
{ name: 'go:yes', color: '0E8A16', description: 'Ready to implement' },
|
|
||||||
{ name: 'go:no', color: 'B60205', description: 'Not pursuing' },
|
|
||||||
{ name: 'go:needs-research', color: 'FBCA04', description: 'Needs investigation' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const RELEASE_LABELS = [
|
|
||||||
{ name: 'release:v0.4.0', color: '6B8EB5', description: 'Targeted for v0.4.0' },
|
|
||||||
{ name: 'release:v0.5.0', color: '6B8EB5', description: 'Targeted for v0.5.0' },
|
|
||||||
{ name: 'release:v0.6.0', color: '8B7DB5', description: 'Targeted for v0.6.0' },
|
|
||||||
{ name: 'release:v1.0.0', color: '8B7DB5', description: 'Targeted for v1.0.0' },
|
|
||||||
{ name: 'release:backlog', color: 'D4E5F7', description: 'Not yet targeted' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const TYPE_LABELS = [
|
|
||||||
{ name: 'type:feature', color: 'DDD1F2', description: 'New capability' },
|
|
||||||
{ name: 'type:bug', color: 'FF0422', description: 'Something broken' },
|
|
||||||
{ name: 'type:spike', color: 'F2DDD4', description: 'Research/investigation — produces a plan, not code' },
|
|
||||||
{ name: 'type:docs', color: 'D4E5F7', description: 'Documentation work' },
|
|
||||||
{ name: 'type:chore', color: 'D4E5F7', description: 'Maintenance, refactoring, cleanup' },
|
|
||||||
{ name: 'type:epic', color: 'CC4455', description: 'Parent issue that decomposes into sub-issues' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// High-signal labels — these MUST visually dominate all others
|
|
||||||
const SIGNAL_LABELS = [
|
|
||||||
{ name: 'bug', color: 'FF0422', description: 'Something isn\'t working' },
|
|
||||||
{ name: 'feedback', color: '00E5FF', description: 'User feedback — high signal, needs attention' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const PRIORITY_LABELS = [
|
|
||||||
{ name: 'priority:p0', color: 'B60205', description: 'Blocking release' },
|
|
||||||
{ name: 'priority:p1', color: 'D93F0B', description: 'This sprint' },
|
|
||||||
{ name: 'priority:p2', color: 'FBCA04', description: 'Next sprint' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Ensure the base "squad" triage label exists
|
|
||||||
const labels = [
|
|
||||||
{ name: 'squad', color: SQUAD_COLOR, description: 'Squad triage inbox — Lead will assign to a member' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const member of members) {
|
|
||||||
labels.push({
|
|
||||||
name: `squad:${member.name.toLowerCase()}`,
|
|
||||||
color: MEMBER_COLOR,
|
|
||||||
description: `Assigned to ${member.name} (${member.role})`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add @copilot label if coding agent is on the team
|
|
||||||
if (hasCopilot) {
|
|
||||||
labels.push({
|
|
||||||
name: 'squad:copilot',
|
|
||||||
color: COPILOT_COLOR,
|
|
||||||
description: 'Assigned to @copilot (Coding Agent) for autonomous work'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add go:, release:, type:, priority:, and high-signal labels
|
|
||||||
labels.push(...GO_LABELS);
|
|
||||||
labels.push(...RELEASE_LABELS);
|
|
||||||
labels.push(...TYPE_LABELS);
|
|
||||||
labels.push(...PRIORITY_LABELS);
|
|
||||||
labels.push(...SIGNAL_LABELS);
|
|
||||||
|
|
||||||
// Sync labels (create or update)
|
|
||||||
for (const label of labels) {
|
|
||||||
try {
|
|
||||||
await github.rest.issues.getLabel({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
name: label.name
|
|
||||||
});
|
|
||||||
// Label exists — update it
|
|
||||||
await github.rest.issues.updateLabel({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
name: label.name,
|
|
||||||
color: label.color,
|
|
||||||
description: label.description
|
|
||||||
});
|
|
||||||
core.info(`Updated label: ${label.name}`);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.status === 404) {
|
|
||||||
// Label doesn't exist — create it
|
|
||||||
await github.rest.issues.createLabel({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
name: label.name,
|
|
||||||
color: label.color,
|
|
||||||
description: label.description
|
|
||||||
});
|
|
||||||
core.info(`Created label: ${label.name}`);
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info(`Label sync complete: ${labels.length} labels synced`);
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -27,7 +27,4 @@ replacements.txt
|
|||||||
reps.txt
|
reps.txt
|
||||||
cmd/server/server.exe
|
cmd/server/server.exe
|
||||||
cmd/ingestor/ingestor.exe
|
cmd/ingestor/ingestor.exe
|
||||||
# CI trigger
|
# CI trigger
|
||||||
!test-fixtures/e2e-fixture.db
|
|
||||||
corescope-server
|
|
||||||
cmd/server/server
|
|
||||||
|
|||||||
10
.nycrc.json
10
.nycrc.json
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"include": [
|
|
||||||
"public/*.js"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"public/vendor/**",
|
|
||||||
"public/leaflet-*.js",
|
|
||||||
"public/qrcode*.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,48 +1,48 @@
|
|||||||
# Bishop — Tester
|
# Bishop — Tester
|
||||||
|
|
||||||
Unit tests, Playwright E2E, coverage gates, and quality assurance for CoreScope.
|
Unit tests, Playwright E2E, coverage gates, and quality assurance for CoreScope.
|
||||||
|
|
||||||
## Project Context
|
## Project Context
|
||||||
|
|
||||||
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
|
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
|
||||||
**Stack:** Node.js native test runner, Playwright, c8 + nyc (coverage), supertest
|
**Stack:** Node.js native test runner, Playwright, c8 + nyc (coverage), supertest
|
||||||
**User:** User
|
**User:** User
|
||||||
|
|
||||||
## Responsibilities
|
## Responsibilities
|
||||||
|
|
||||||
- Unit tests: test-packet-filter.js, test-aging.js, test-decoder.js, test-decoder-spec.js, test-server-helpers.js, test-server-routes.js, test-packet-store.js, test-db.js, test-frontend-helpers.js, test-regional-filter.js, test-regional-integration.js, test-live-dedup.js
|
- Unit tests: test-packet-filter.js, test-aging.js, test-decoder.js, test-decoder-spec.js, test-server-helpers.js, test-server-routes.js, test-packet-store.js, test-db.js, test-frontend-helpers.js, test-regional-filter.js, test-regional-integration.js, test-live-dedup.js
|
||||||
- Playwright E2E: test-e2e-playwright.js (8 browser tests, default localhost:3000)
|
- Playwright E2E: test-e2e-playwright.js (8 browser tests, default localhost:3000)
|
||||||
- E2E tools: tools/e2e-test.js, tools/frontend-test.js
|
- E2E tools: tools/e2e-test.js, tools/frontend-test.js
|
||||||
- Coverage: Backend 85%+ (c8), Frontend 42%+ (Istanbul + nyc). Both only go up.
|
- Coverage: Backend 85%+ (c8), Frontend 42%+ (Istanbul + nyc). Both only go up.
|
||||||
- Review authority: May approve or reject work from Hicks and Newt based on test results
|
- Review authority: May approve or reject work from Hicks and Newt based on test results
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
- Test the REAL code — import actual modules, don't copy-paste functions into test files
|
- Test the REAL code — import actual modules, don't copy-paste functions into test files
|
||||||
- Use vm.createContext for frontend helpers (see test-frontend-helpers.js pattern)
|
- Use vm.createContext for frontend helpers (see test-frontend-helpers.js pattern)
|
||||||
- Playwright tests default to localhost:3000 — NEVER run against prod
|
- Playwright tests default to localhost:3000 — NEVER run against prod
|
||||||
- Every bug fix gets a regression test
|
- Every bug fix gets a regression test
|
||||||
- Every new feature must add tests — test count only goes up
|
- Every new feature must add tests — test count only goes up
|
||||||
- Run `npm test` to verify all tests pass before approving
|
- Run `npm test` to verify all tests pass before approving
|
||||||
|
|
||||||
## Review Authority
|
## Review Authority
|
||||||
|
|
||||||
- May approve or reject based on test coverage and quality
|
- May approve or reject based on test coverage and quality
|
||||||
- On rejection: specify what tests are missing or failing
|
- On rejection: specify what tests are missing or failing
|
||||||
- Lockout rules apply
|
- Lockout rules apply
|
||||||
|
|
||||||
## Key Test Commands
|
## Key Test Commands
|
||||||
|
|
||||||
```
|
```
|
||||||
npm test # all backend tests + coverage summary
|
npm test # all backend tests + coverage summary
|
||||||
npm run test:unit # fast: unit tests only
|
npm run test:unit # fast: unit tests only
|
||||||
npm run test:coverage # all tests + HTML coverage report
|
npm run test:coverage # all tests + HTML coverage report
|
||||||
node test-packet-filter.js # filter engine
|
node test-packet-filter.js # filter engine
|
||||||
node test-decoder.js # packet decoder
|
node test-decoder.js # packet decoder
|
||||||
node test-server-routes.js # API routes via supertest
|
node test-server-routes.js # API routes via supertest
|
||||||
node test-e2e-playwright.js # 8 Playwright browser tests
|
node test-e2e-playwright.js # 8 Playwright browser tests
|
||||||
```
|
```
|
||||||
|
|
||||||
## Model
|
## Model
|
||||||
|
|
||||||
Preferred: auto
|
Preferred: auto
|
||||||
|
|||||||
@@ -1,76 +1,76 @@
|
|||||||
# Bishop — History
|
# Bishop — History
|
||||||
|
|
||||||
## Project Context
|
## Project Context
|
||||||
|
|
||||||
CoreScope has 14 test files, 4,290 lines of test code. Backend coverage 85%+, frontend 42%+. Tests use Node.js native runner, Playwright for E2E, c8/nyc for coverage, supertest for API routes. vm.createContext pattern used for testing frontend helpers in Node.js.
|
CoreScope has 14 test files, 4,290 lines of test code. Backend coverage 85%+, frontend 42%+. Tests use Node.js native runner, Playwright for E2E, c8/nyc for coverage, supertest for API routes. vm.createContext pattern used for testing frontend helpers in Node.js.
|
||||||
|
|
||||||
User: User
|
User: User
|
||||||
|
|
||||||
## Learnings
|
## Learnings
|
||||||
|
|
||||||
- Session started 2026-03-26. Team formed: Kobayashi (Lead), Hicks (Backend), Newt (Frontend), Bishop (Tester).
|
- Session started 2026-03-26. Team formed: Kobayashi (Lead), Hicks (Backend), Newt (Frontend), Bishop (Tester).
|
||||||
- E2E run 2026-03-26: 12/16 passed, 4 failed. Results:
|
- E2E run 2026-03-26: 12/16 passed, 4 failed. Results:
|
||||||
- ✅ Home page loads
|
- ✅ Home page loads
|
||||||
- ✅ Nodes page loads with data
|
- ✅ Nodes page loads with data
|
||||||
- ❌ Map page loads with markers — No markers found (empty DB, no geo data)
|
- ❌ Map page loads with markers — No markers found (empty DB, no geo data)
|
||||||
- ✅ Packets page loads with filter
|
- ✅ Packets page loads with filter
|
||||||
- ✅ Node detail loads
|
- ✅ Node detail loads
|
||||||
- ✅ Theme customizer opens
|
- ✅ Theme customizer opens
|
||||||
- ✅ Dark mode toggle
|
- ✅ Dark mode toggle
|
||||||
- ✅ Analytics page loads
|
- ✅ Analytics page loads
|
||||||
- ✅ Map heat checkbox persists in localStorage
|
- ✅ Map heat checkbox persists in localStorage
|
||||||
- ✅ Map heat checkbox is clickable
|
- ✅ Map heat checkbox is clickable
|
||||||
- ✅ Live heat disabled when ghosts mode active
|
- ✅ Live heat disabled when ghosts mode active
|
||||||
- ✅ Live heat checkbox persists in localStorage
|
- ✅ Live heat checkbox persists in localStorage
|
||||||
- ✅ Heatmap opacity persists in localStorage
|
- ✅ Heatmap opacity persists in localStorage
|
||||||
- ❌ Live heatmap opacity persists — browser closed before test ran (bug: browser.close() on line 274 is before tests 14-16)
|
- ❌ Live heatmap opacity persists — browser closed before test ran (bug: browser.close() on line 274 is before tests 14-16)
|
||||||
- ❌ Customizer has separate map/live opacity sliders — same browser-closed bug
|
- ❌ Customizer has separate map/live opacity sliders — same browser-closed bug
|
||||||
- ❌ Map re-renders on resize — same browser-closed bug
|
- ❌ Map re-renders on resize — same browser-closed bug
|
||||||
- BUG FOUND: test-e2e-playwright.js line 274 calls `await browser.close()` before tests 14, 15, 16 execute. Those 3 tests will always fail. The `browser.close()` must be moved after all tests.
|
- BUG FOUND: test-e2e-playwright.js line 274 calls `await browser.close()` before tests 14, 15, 16 execute. Those 3 tests will always fail. The `browser.close()` must be moved after all tests.
|
||||||
- The "Map page loads with markers" failure is expected with an empty local DB — no nodes with coordinates exist to render markers.
|
- The "Map page loads with markers" failure is expected with an empty local DB — no nodes with coordinates exist to render markers.
|
||||||
- FIX APPLIED 2026-03-26: Moved `browser.close()` from between test 13 and test 14 to after test 16 (just before the summary). Tests 14 ("Live heatmap opacity persists") and 15 ("Customizer has separate map/live opacity sliders") now pass. Test 16 ("Map re-renders on resize") now runs but fails due to empty DB (no markers to count) — same root cause as test 3. Result: 14/16 pass, 2 fail (both map-marker tests, expected with empty DB).
|
- FIX APPLIED 2026-03-26: Moved `browser.close()` from between test 13 and test 14 to after test 16 (just before the summary). Tests 14 ("Live heatmap opacity persists") and 15 ("Customizer has separate map/live opacity sliders") now pass. Test 16 ("Map re-renders on resize") now runs but fails due to empty DB (no markers to count) — same root cause as test 3. Result: 14/16 pass, 2 fail (both map-marker tests, expected with empty DB).
|
||||||
- TESTS ADDED 2026-03-26: Issue #127 (copyToClipboard) — 8 unit tests in test-frontend-helpers.js using vm.createContext + DOM/clipboard mocks. Tests cover: fallback path (execCommand success/fail/throw), clipboard API path, null/undefined input, textarea lifecycle, no-callback usage. Pattern: `makeClipboardSandbox(opts)` helper builds sandbox with configurable navigator.clipboard and document.execCommand mocks. Total frontend helper tests: 47→55.
|
- TESTS ADDED 2026-03-26: Issue #127 (copyToClipboard) — 8 unit tests in test-frontend-helpers.js using vm.createContext + DOM/clipboard mocks. Tests cover: fallback path (execCommand success/fail/throw), clipboard API path, null/undefined input, textarea lifecycle, no-callback usage. Pattern: `makeClipboardSandbox(opts)` helper builds sandbox with configurable navigator.clipboard and document.execCommand mocks. Total frontend helper tests: 47→55.
|
||||||
- TESTS ADDED 2026-03-26: Issue #125 (packet detail dismiss) — 1 E2E test in test-e2e-playwright.js. Tests: click row → pane opens (empty class removed) → click ✕ → pane closes (empty class restored). Skips gracefully when DB has no packets. Inserted before analytics group, before browser.close().
|
- TESTS ADDED 2026-03-26: Issue #125 (packet detail dismiss) — 1 E2E test in test-e2e-playwright.js. Tests: click row → pane opens (empty class removed) → click ✕ → pane closes (empty class restored). Skips gracefully when DB has no packets. Inserted before analytics group, before browser.close().
|
||||||
- E2E SPEED OPTIMIZATION 2026-03-26: Rewrote test-e2e-playwright.js for performance per Kobayashi's audit. Changes:
|
- E2E SPEED OPTIMIZATION 2026-03-26: Rewrote test-e2e-playwright.js for performance per Kobayashi's audit. Changes:
|
||||||
- Replaced ALL 19 `waitUntil: 'networkidle'` → `'domcontentloaded'` + targeted `waitForSelector`/`waitForFunction`. networkidle stalls ~500ms+ per navigation due to persistent WebSocket + Leaflet tiles.
|
- Replaced ALL 19 `waitUntil: 'networkidle'` → `'domcontentloaded'` + targeted `waitForSelector`/`waitForFunction`. networkidle stalls ~500ms+ per navigation due to persistent WebSocket + Leaflet tiles.
|
||||||
- Eliminated 11 of 12 `waitForTimeout` sleeps → event-driven waits (waitForSelector, waitForFunction). Only 1 remains: 500ms for packet filter debounce (was 1500ms).
|
- Eliminated 11 of 12 `waitForTimeout` sleeps → event-driven waits (waitForSelector, waitForFunction). Only 1 remains: 500ms for packet filter debounce (was 1500ms).
|
||||||
- Reordered tests into page groups to eliminate 7 redundant navigations (page.goto 14→7): Home(1,6,7), Nodes(2,5), Map(3,9,10,13,16), Packets(4), Analytics(8), Live(11,12), NoNav(14,15).
|
- Reordered tests into page groups to eliminate 7 redundant navigations (page.goto 14→7): Home(1,6,7), Nodes(2,5), Map(3,9,10,13,16), Packets(4), Analytics(8), Live(11,12), NoNav(14,15).
|
||||||
- Reduced default timeout from 15s to 10s.
|
- Reduced default timeout from 15s to 10s.
|
||||||
- All 17 test names and assertions preserved unchanged.
|
- All 17 test names and assertions preserved unchanged.
|
||||||
- Verified: 17/17 tests pass against local server with generated test data.
|
- Verified: 17/17 tests pass against local server with generated test data.
|
||||||
- COVERAGE PIPELINE TIMING (measured locally, Windows):
|
- COVERAGE PIPELINE TIMING (measured locally, Windows):
|
||||||
- Phase 1: Istanbul instrumentation (22 JS files) — **3.7s**
|
- Phase 1: Istanbul instrumentation (22 JS files) — **3.7s**
|
||||||
- Phase 2: Server startup (COVERAGE=1) — **~2s** (ready after pre-warm)
|
- Phase 2: Server startup (COVERAGE=1) — **~2s** (ready after pre-warm)
|
||||||
- Phase 3: Playwright E2E (test-e2e-playwright.js, 17 tests) — **3.7s**
|
- Phase 3: Playwright E2E (test-e2e-playwright.js, 17 tests) — **3.7s**
|
||||||
- Phase 4: Coverage collector (collect-frontend-coverage.js) — **746s (12.4 min)** ← THE BOTTLENECK
|
- Phase 4: Coverage collector (collect-frontend-coverage.js) — **746s (12.4 min)** ← THE BOTTLENECK
|
||||||
- Phase 5: nyc report generation — **1.8s**
|
- Phase 5: nyc report generation — **1.8s**
|
||||||
- TOTAL: ~757s (~12.6 min locally). CI reports ~13 min (matches).
|
- TOTAL: ~757s (~12.6 min locally). CI reports ~13 min (matches).
|
||||||
- ROOT CAUSE: collect-frontend-coverage.js is a 978-line script that launches a SECOND Playwright browser and exhaustively clicks every UI element on every page to maximize code coverage. It contains:
|
- ROOT CAUSE: collect-frontend-coverage.js is a 978-line script that launches a SECOND Playwright browser and exhaustively clicks every UI element on every page to maximize code coverage. It contains:
|
||||||
- 169 explicit `waitForTimeout()` calls totaling 104.1s (1.74 min) of hard sleep
|
- 169 explicit `waitForTimeout()` calls totaling 104.1s (1.74 min) of hard sleep
|
||||||
- 21 `waitUntil: 'networkidle'` navigations (each adds ~2-15s depending on page load + WebSocket/tile activity)
|
- 21 `waitUntil: 'networkidle'` navigations (each adds ~2-15s depending on page load + WebSocket/tile activity)
|
||||||
- Visits 12 pages: Home, Nodes, Packets, Map, Analytics, Customizer, Channels, Live, Traces, Observers, Perf, plus global router/theme exercises
|
- Visits 12 pages: Home, Nodes, Packets, Map, Analytics, Customizer, Channels, Live, Traces, Observers, Perf, plus global router/theme exercises
|
||||||
- Heaviest sections by sleep: Packets (13s), Analytics (13.8s), Nodes (11.6s), Live (11.7s), App.js router (10.4s)
|
- Heaviest sections by sleep: Packets (13s), Analytics (13.8s), Nodes (11.6s), Live (11.7s), App.js router (10.4s)
|
||||||
- The networkidle waits are the real killer — they stall ~500ms-15s EACH waiting for WebSocket + Leaflet tiles to settle
|
- The networkidle waits are the real killer — they stall ~500ms-15s EACH waiting for WebSocket + Leaflet tiles to settle
|
||||||
- Note: test-e2e-interactions.js (called in combined-coverage.sh) does not exist — it fails silently via `|| true`
|
- Note: test-e2e-interactions.js (called in combined-coverage.sh) does not exist — it fails silently via `|| true`
|
||||||
- OPTIMIZATION OPPORTUNITIES: Replace networkidle→domcontentloaded (same fix as E2E tests), replace waitForTimeout with event-driven waits, reduce/batch page navigations, parallelize independent page exercises
|
- OPTIMIZATION OPPORTUNITIES: Replace networkidle→domcontentloaded (same fix as E2E tests), replace waitForTimeout with event-driven waits, reduce/batch page navigations, parallelize independent page exercises
|
||||||
- REGRESSION TESTS ADDED 2026-03-27: Memory optimization (observation deduplication). 8 new tests in test-packet-store.js under "=== Observation deduplication (transmission_id refs) ===" section. Tests verify: (1) observations don't duplicate raw_hex/decoded_json, (2) transmission fields accessible via store.byTxId.get(obs.transmission_id), (3) query() and all() still return transmission fields for backward compat, (4) multiple observations share one transmission_id, (5) getSiblings works after dedup, (6) queryGrouped returns transmission fields, (7) memory estimate reflects dedup savings. 4 tests fail pre-fix (expected — Hicks hasn't applied changes yet), 4 pass (backward compat). Pattern: use hasOwnProperty() to distinguish own vs inherited/absent fields.
|
- REGRESSION TESTS ADDED 2026-03-27: Memory optimization (observation deduplication). 8 new tests in test-packet-store.js under "=== Observation deduplication (transmission_id refs) ===" section. Tests verify: (1) observations don't duplicate raw_hex/decoded_json, (2) transmission fields accessible via store.byTxId.get(obs.transmission_id), (3) query() and all() still return transmission fields for backward compat, (4) multiple observations share one transmission_id, (5) getSiblings works after dedup, (6) queryGrouped returns transmission fields, (7) memory estimate reflects dedup savings. 4 tests fail pre-fix (expected — Hicks hasn't applied changes yet), 4 pass (backward compat). Pattern: use hasOwnProperty() to distinguish own vs inherited/absent fields.
|
||||||
- REVIEW 2026-03-27: Hicks RAM fix (observation dedup). REJECTED. Tests pass (42 packet-store + 204 route), but 5 server.js consumers access `.hash`, `.raw_hex`, `.decoded_json`, `.payload_type` on lean observations from `byObserver.get()` or `tx.observations` without enrichment. Broken endpoints: (1) `/api/nodes/bulk-health` line 1141 `o.hash` undefined, (2) `/api/nodes/network-status` line 1220 `o.hash` undefined, (3) `/api/analytics/signal` lines 1298+1306 `p.hash`/`p.raw_hex` undefined, (4) `/api/observers/:id/analytics` lines 2320+2329+2361 `p.payload_type`/`p.decoded_json` undefined + lean objects sent to client as recentPackets, (5) `/api/analytics/subpaths` line 2711 `o.hash` undefined. All are regional filtering or analytics code paths that use `byObserver` directly. Fix: either enrich at these call sites or store `hash` on observations (it's small). The enrichment pattern works for `getById()`, `getSiblings()`, and `/api/packets/:id` but was not applied to the 5 other consumers. Route tests pass because they don't assert on these specific field values in analytics responses.
|
- REVIEW 2026-03-27: Hicks RAM fix (observation dedup). REJECTED. Tests pass (42 packet-store + 204 route), but 5 server.js consumers access `.hash`, `.raw_hex`, `.decoded_json`, `.payload_type` on lean observations from `byObserver.get()` or `tx.observations` without enrichment. Broken endpoints: (1) `/api/nodes/bulk-health` line 1141 `o.hash` undefined, (2) `/api/nodes/network-status` line 1220 `o.hash` undefined, (3) `/api/analytics/signal` lines 1298+1306 `p.hash`/`p.raw_hex` undefined, (4) `/api/observers/:id/analytics` lines 2320+2329+2361 `p.payload_type`/`p.decoded_json` undefined + lean objects sent to client as recentPackets, (5) `/api/analytics/subpaths` line 2711 `o.hash` undefined. All are regional filtering or analytics code paths that use `byObserver` directly. Fix: either enrich at these call sites or store `hash` on observations (it's small). The enrichment pattern works for `getById()`, `getSiblings()`, and `/api/packets/:id` but was not applied to the 5 other consumers. Route tests pass because they don't assert on these specific field values in analytics responses.
|
||||||
- BATCH REVIEW 2026-03-27: Reviewed 6 issue fixes pushed without sign-off. Full suite: 971 tests, 0 failures across 11 test files. Cache busters uniform (v=1774625000). Verdicts:
|
- BATCH REVIEW 2026-03-27: Reviewed 6 issue fixes pushed without sign-off. Full suite: 971 tests, 0 failures across 11 test files. Cache busters uniform (v=1774625000). Verdicts:
|
||||||
- #133 (phantom nodes): ✅ APPROVED. 12 assertions on removePhantomNodes, real db.js code, edge cases (idempotency, real node preserved, stats filtering).
|
- #133 (phantom nodes): ✅ APPROVED. 12 assertions on removePhantomNodes, real db.js code, edge cases (idempotency, real node preserved, stats filtering).
|
||||||
- #123 (channel hash): ⚠️ APPROVED WITH NOTES. 6 new decoder tests cover channelHashHex (zero-padding) and decryptionStatus (no_key ×3, decryption_failed). Missing: `decrypted` status untested (needs valid crypto key), frontend rendering of "Ch 0xXX (no key)" untested.
|
- #123 (channel hash): ⚠️ APPROVED WITH NOTES. 6 new decoder tests cover channelHashHex (zero-padding) and decryptionStatus (no_key ×3, decryption_failed). Missing: `decrypted` status untested (needs valid crypto key), frontend rendering of "Ch 0xXX (no key)" untested.
|
||||||
- #126 (offline node on map): ✅ APPROVED. 3 regression tests: ambiguous prefix→null, unique prefix→resolves, dead node stays dead. Caching verified. Excellent quality.
|
- #126 (offline node on map): ✅ APPROVED. 3 regression tests: ambiguous prefix→null, unique prefix→resolves, dead node stays dead. Caching verified. Excellent quality.
|
||||||
- #130 (disappearing nodes): ✅ APPROVED. 8 pruneStaleNodes tests cover dim/restore/remove for API vs WS nodes. Real live.js via vm.createContext.
|
- #130 (disappearing nodes): ✅ APPROVED. 8 pruneStaleNodes tests cover dim/restore/remove for API vs WS nodes. Real live.js via vm.createContext.
|
||||||
- #131 (auto-updating nodes): ⚠️ APPROVED WITH NOTES. 8 solid isAdvertMessage tests (real code). BUT 5 WS handler tests are source-string-match checks (`src.includes('loadNodes(true)')`) — these verify code exists but not that it works at runtime. No runtime test for debounce batching behavior.
|
- #131 (auto-updating nodes): ⚠️ APPROVED WITH NOTES. 8 solid isAdvertMessage tests (real code). BUT 5 WS handler tests are source-string-match checks (`src.includes('loadNodes(true)')`) — these verify code exists but not that it works at runtime. No runtime test for debounce batching behavior.
|
||||||
- #129 (observer comparison): ✅ APPROVED. 11 comprehensive tests for comparePacketSets — all edge cases, performance (10K hashes <500ms), mathematical invariant. Real compare.js via vm.createContext.
|
- #129 (observer comparison): ✅ APPROVED. 11 comprehensive tests for comparePacketSets — all edge cases, performance (10K hashes <500ms), mathematical invariant. Real compare.js via vm.createContext.
|
||||||
- NOTES FOR IMPROVEMENT: (1) #131 debounce behavior should get a runtime test via vm.createContext, not string checks. (2) #123 could benefit from a `decrypted` status test if crypto mocking is feasible. Neither is blocking.
|
- NOTES FOR IMPROVEMENT: (1) #131 debounce behavior should get a runtime test via vm.createContext, not string checks. (2) #123 could benefit from a `decrypted` status test if crypto mocking is feasible. Neither is blocking.
|
||||||
- TEST GAP FIX 2026-03-27: Closed both noted gaps from batch review:
|
- TEST GAP FIX 2026-03-27: Closed both noted gaps from batch review:
|
||||||
- #123 (channel hash decryption `decrypted` status): 3 new tests in test-decoder.js. Used require.cache mocking to swap ChannelCrypto module with mock that returns `{success:true, data:{...}}`. Tests cover: (1) decrypted status with sender+message (text formatted as "Sender: message"), (2) decrypted without sender (text is just message), (3) multiple keys tried, first match wins (verifies iteration order + call count). All verify channelHashHex, type='CHAN', channel name, sender, timestamp, flags. require.cache is restored in finally block.
|
- #123 (channel hash decryption `decrypted` status): 3 new tests in test-decoder.js. Used require.cache mocking to swap ChannelCrypto module with mock that returns `{success:true, data:{...}}`. Tests cover: (1) decrypted status with sender+message (text formatted as "Sender: message"), (2) decrypted without sender (text is just message), (3) multiple keys tried, first match wins (verifies iteration order + call count). All verify channelHashHex, type='CHAN', channel name, sender, timestamp, flags. require.cache is restored in finally block.
|
||||||
- #131 (WS handler runtime tests): Rewrote 5 `src.includes()` string-match tests to use vm.createContext with runtime execution. Created `makeNodesWsSandbox()` helper that provides controllable setTimeout (timer queue), mock DOM, tracked api/invalidateApiCache calls, and real `debouncedOnWS` logic. Tests run actual nodes.js init() and verify: (1) ADVERT triggers refresh with 5s debounce, (2) non-ADVERT doesn't trigger refresh, (3) debounce collapses 3 ADVERTs into 1 API call, (4) _allNodes cache reset forces re-fetch, (5) scroll/selection preserved (panel innerHTML + scrollTop untouched by WS handler). Total: 87 frontend helper tests (same count — 5 replaced, not added), 61 decoder tests (+3).
|
- #131 (WS handler runtime tests): Rewrote 5 `src.includes()` string-match tests to use vm.createContext with runtime execution. Created `makeNodesWsSandbox()` helper that provides controllable setTimeout (timer queue), mock DOM, tracked api/invalidateApiCache calls, and real `debouncedOnWS` logic. Tests run actual nodes.js init() and verify: (1) ADVERT triggers refresh with 5s debounce, (2) non-ADVERT doesn't trigger refresh, (3) debounce collapses 3 ADVERTs into 1 API call, (4) _allNodes cache reset forces re-fetch, (5) scroll/selection preserved (panel innerHTML + scrollTop untouched by WS handler). Total: 87 frontend helper tests (same count — 5 replaced, not added), 61 decoder tests (+3).
|
||||||
- Technique learned: require.cache mocking is effective for testing code paths that depend on external modules (like ChannelCrypto). Store original, replace exports, restore in finally. Controllable setTimeout (capturing callbacks in array, firing manually) enables testing debounce logic without real timers.
|
- Technique learned: require.cache mocking is effective for testing code paths that depend on external modules (like ChannelCrypto). Store original, replace exports, restore in finally. Controllable setTimeout (capturing callbacks in array, firing manually) enables testing debounce logic without real timers.
|
||||||
|
|
||||||
- **Massive session 2026-03-27 (FULL DAY):** Reviewed and approved all 6 fixes, closed 2 test gaps, validated E2E:
|
- **Massive session 2026-03-27 (FULL DAY):** Reviewed and approved all 6 fixes, closed 2 test gaps, validated E2E:
|
||||||
- **Batch PR review:** #123 (channel hash), #126 (ambiguous prefixes), #130 (live map), #131 (WS auto-update), #129 (observer comparison) — 2 gaps identified, resolved.
|
- **Batch PR review:** #123 (channel hash), #126 (ambiguous prefixes), #130 (live map), #131 (WS auto-update), #129 (observer comparison) — 2 gaps identified, resolved.
|
||||||
- **Gap 1 closed:** #123 decrypted status mocked via require.cache (ChannelCrypto module swap). 3 new decoder tests.
|
- **Gap 1 closed:** #123 decrypted status mocked via require.cache (ChannelCrypto module swap). 3 new decoder tests.
|
||||||
- **Gap 2 closed:** #131 WS debounce runtime tests via vm.createContext. 5 source-match tests replaced with actual execution tests. Controllable setTimeout technique verified.
|
- **Gap 2 closed:** #131 WS debounce runtime tests via vm.createContext. 5 source-match tests replaced with actual execution tests. Controllable setTimeout technique verified.
|
||||||
- **Test counts:** 109 db tests (+14 phantom), 204 route tests (+5 WS), 90 frontend tests (+3 pane), 61 decoder tests (+3 channel), 25 Go ingestor tests, 42 Go server tests.
|
- **Test counts:** 109 db tests (+14 phantom), 204 route tests (+5 WS), 90 frontend tests (+3 pane), 61 decoder tests (+3 channel), 25 Go ingestor tests, 42 Go server tests.
|
||||||
- **E2E validation:** 16 Playwright tests passing, all routes functional with merged 1.237M observation DB. Browser smoke tests verified. Coverage 85%+ backend, 42%+ frontend.
|
- **E2E validation:** 16 Playwright tests passing, all routes functional with merged 1.237M observation DB. Browser smoke tests verified. Coverage 85%+ backend, 42%+ frontend.
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
# Hicks — Backend Dev
|
# Hicks — Backend Dev
|
||||||
|
|
||||||
Server, decoder, packet-store, SQLite, API, MQTT, WebSocket, and performance for CoreScope.
|
Server, decoder, packet-store, SQLite, API, MQTT, WebSocket, and performance for CoreScope.
|
||||||
|
|
||||||
## Project Context
|
## Project Context
|
||||||
|
|
||||||
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
|
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
|
||||||
**Stack:** Node.js 18+, Express 5, SQLite (better-sqlite3), MQTT (mqtt), WebSocket (ws)
|
**Stack:** Node.js 18+, Express 5, SQLite (better-sqlite3), MQTT (mqtt), WebSocket (ws)
|
||||||
**User:** User
|
**User:** User
|
||||||
|
|
||||||
## Responsibilities
|
## Responsibilities
|
||||||
|
|
||||||
- server.js — Express API routes, MQTT ingestion, WebSocket broadcast
|
- server.js — Express API routes, MQTT ingestion, WebSocket broadcast
|
||||||
- decoder.js — Custom MeshCore packet parser (header, path, payload, adverts)
|
- decoder.js — Custom MeshCore packet parser (header, path, payload, adverts)
|
||||||
- packet-store.js — In-memory ring buffer + indexes (O(1) lookups)
|
- packet-store.js — In-memory ring buffer + indexes (O(1) lookups)
|
||||||
- db.js — SQLite schema, prepared statements, migrations
|
- db.js — SQLite schema, prepared statements, migrations
|
||||||
- server-helpers.js — Shared backend helpers (health checks, geo distance)
|
- server-helpers.js — Shared backend helpers (health checks, geo distance)
|
||||||
- Performance optimization — caching, response times, no O(n²)
|
- Performance optimization — caching, response times, no O(n²)
|
||||||
- Docker/deployment — Dockerfile, manage.sh, docker-compose
|
- Docker/deployment — Dockerfile, manage.sh, docker-compose
|
||||||
- MeshCore protocol — read firmware source before protocol changes
|
- MeshCore protocol — read firmware source before protocol changes
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
- Do NOT modify frontend files (public/*.js, public/*.css, index.html)
|
- Do NOT modify frontend files (public/*.js, public/*.css, index.html)
|
||||||
- Always read AGENTS.md before starting work
|
- Always read AGENTS.md before starting work
|
||||||
- Always read firmware source (firmware/src/) before protocol changes
|
- Always read firmware source (firmware/src/) before protocol changes
|
||||||
- Run `npm test` before considering work done
|
- Run `npm test` before considering work done
|
||||||
- Cache busters are Newt's job, but flag if you change an API response shape
|
- Cache busters are Newt's job, but flag if you change an API response shape
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
- server.js (2,661 lines) — main backend
|
- server.js (2,661 lines) — main backend
|
||||||
- decoder.js (320 lines) — packet parser
|
- decoder.js (320 lines) — packet parser
|
||||||
- packet-store.js (668 lines) — in-memory store
|
- packet-store.js (668 lines) — in-memory store
|
||||||
- db.js (743 lines) — SQLite layer
|
- db.js (743 lines) — SQLite layer
|
||||||
- server-helpers.js (289 lines) — shared helpers
|
- server-helpers.js (289 lines) — shared helpers
|
||||||
- iata-coords.js — airport coordinates for regional filtering
|
- iata-coords.js — airport coordinates for regional filtering
|
||||||
|
|
||||||
## Model
|
## Model
|
||||||
|
|
||||||
Preferred: auto
|
Preferred: auto
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
# Hicks — History
|
# Hicks — History
|
||||||
|
|
||||||
## Project Context
|
## Project Context
|
||||||
|
|
||||||
CoreScope is a real-time LoRa mesh packet analyzer. Node.js + Express + SQLite backend, vanilla JS SPA frontend. Custom decoder.js fixes path_length bug from upstream library. In-memory packet store provides O(1) lookups for 30K+ packets. TTL response cache achieves 7,000× speedup on bulk health endpoint.
|
CoreScope is a real-time LoRa mesh packet analyzer. Node.js + Express + SQLite backend, vanilla JS SPA frontend. Custom decoder.js fixes path_length bug from upstream library. In-memory packet store provides O(1) lookups for 30K+ packets. TTL response cache achieves 7,000× speedup on bulk health endpoint.
|
||||||
|
|
||||||
User: User
|
User: User
|
||||||
|
|
||||||
## Learnings
|
## Learnings
|
||||||
|
|
||||||
- Session started 2026-03-26. Team formed: Kobayashi (Lead), Hicks (Backend), Newt (Frontend), Bishop (Tester).
|
- Session started 2026-03-26. Team formed: Kobayashi (Lead), Hicks (Backend), Newt (Frontend), Bishop (Tester).
|
||||||
- Split the monolithic "Frontend coverage (instrumented Playwright)" CI step into 5 discrete steps: Instrument frontend JS, Start test server (with health-check poll replacing sleep 5), Run Playwright E2E tests, Extract coverage + generate report, Stop test server. Cleanup/report steps use `if: always()` so server shutdown happens even on test failure. Server PID shared across steps via .server.pid file. "Frontend E2E only" fast-path left untouched.
|
- Split the monolithic "Frontend coverage (instrumented Playwright)" CI step into 5 discrete steps: Instrument frontend JS, Start test server (with health-check poll replacing sleep 5), Run Playwright E2E tests, Extract coverage + generate report, Stop test server. Cleanup/report steps use `if: always()` so server shutdown happens even on test failure. Server PID shared across steps via .server.pid file. "Frontend E2E only" fast-path left untouched.
|
||||||
- Fixed memory explosion in packet-store.js: observations no longer duplicate transmission fields (hash, raw_hex, decoded_json, payload_type, route_type). Instead, observations store only `transmission_id` as a reference. Added `_enrichObs()` to hydrate observations at API boundaries (getById, getSiblings, enrichObservations). Replaced `.all()` with `.iterate()` for streaming load. Updated `_transmissionsForObserver()` to use transmission_id instead of hash. For a 185MB DB with 50K transmissions × 23 observations avg, this eliminates ~1.17M copies of hex dumps and JSON — projected ~2GB RAM savings.
|
- Fixed memory explosion in packet-store.js: observations no longer duplicate transmission fields (hash, raw_hex, decoded_json, payload_type, route_type). Instead, observations store only `transmission_id` as a reference. Added `_enrichObs()` to hydrate observations at API boundaries (getById, getSiblings, enrichObservations). Replaced `.all()` with `.iterate()` for streaming load. Updated `_transmissionsForObserver()` to use transmission_id instead of hash. For a 185MB DB with 50K transmissions × 23 observations avg, this eliminates ~1.17M copies of hex dumps and JSON — projected ~2GB RAM savings.
|
||||||
- Built standalone Go MQTT ingestor (`cmd/ingestor/`). Ported decoder.js → Go (header parsing, path extraction, all payload types, advert decoding with flags/lat/lon/name). Ported db.js v3 schema (transmissions + observations + nodes + observers). Ported computeContentHash (SHA-256 based, path-independent). Uses modernc.org/sqlite (pure Go, no CGO) and paho.mqtt.golang. 25 tests passing (decoder golden fixtures from production data + DB schema compatibility). Supports same config.json format as Node.js server. Handles Format 1 (raw packet) messages; companion bridge format deferred. System Go was 1.17 — installed Go 1.22.5 to support modern dependencies.
|
- Built standalone Go MQTT ingestor (`cmd/ingestor/`). Ported decoder.js → Go (header parsing, path extraction, all payload types, advert decoding with flags/lat/lon/name). Ported db.js v3 schema (transmissions + observations + nodes + observers). Ported computeContentHash (SHA-256 based, path-independent). Uses modernc.org/sqlite (pure Go, no CGO) and paho.mqtt.golang. 25 tests passing (decoder golden fixtures from production data + DB schema compatibility). Supports same config.json format as Node.js server. Handles Format 1 (raw packet) messages; companion bridge format deferred. System Go was 1.17 — installed Go 1.22.5 to support modern dependencies.
|
||||||
- Built standalone Go web server (`cmd/server/`) — READ side of the Go rewrite. 35+ REST API endpoints ported from server.js. All queries go directly to SQLite (no in-memory packet store). WebSocket broadcast via SQLite polling. Static file server with SPA fallback. Uses gorilla/mux for routing, gorilla/websocket for WS, modernc.org/sqlite for DB. 42 tests passing (20 DB query tests, 20+ route integration tests, 2 WebSocket tests). `go vet` clean. Binary compiles to single executable. Analytics endpoints that required Node.js in-memory store (topology, distance, hash-sizes, subpaths) return structural stubs — core data (RF stats, channels, node health, etc.) fully functional via SQL. System Go 1.17 → installed Go 1.22 for build. Each cmd/* module has its own go.mod (no root-level go.mod).
|
- Built standalone Go web server (`cmd/server/`) — READ side of the Go rewrite. 35+ REST API endpoints ported from server.js. All queries go directly to SQLite (no in-memory packet store). WebSocket broadcast via SQLite polling. Static file server with SPA fallback. Uses gorilla/mux for routing, gorilla/websocket for WS, modernc.org/sqlite for DB. 42 tests passing (20 DB query tests, 20+ route integration tests, 2 WebSocket tests). `go vet` clean. Binary compiles to single executable. Analytics endpoints that required Node.js in-memory store (topology, distance, hash-sizes, subpaths) return structural stubs — core data (RF stats, channels, node health, etc.) fully functional via SQL. System Go 1.17 → installed Go 1.22 for build. Each cmd/* module has its own go.mod (no root-level go.mod).
|
||||||
- Go server API parity fix: Rewrote QueryPackets from observation-centric (packets_v view) to transmission-centric (transmissions table + correlated subqueries). This fixes both performance (9s to sub-100ms for unfiltered queries on 1.2M rows) and response shape. Packets now return first_seen, timestamp (= first_seen), observation_count, and NOT created_at/payload_version/score. Node responses now include last_heard (= last_seen fallback), hash_size (null), hash_size_inconsistent (false). Added schema version detection (v2 vs v3 observations table). Fixed QueryGroupedPackets first_seen. Added GetRecentTransmissionsForNode. All tests pass, build clean with Go 1.22.
|
- Go server API parity fix: Rewrote QueryPackets from observation-centric (packets_v view) to transmission-centric (transmissions table + correlated subqueries). This fixes both performance (9s to sub-100ms for unfiltered queries on 1.2M rows) and response shape. Packets now return first_seen, timestamp (= first_seen), observation_count, and NOT created_at/payload_version/score. Node responses now include last_heard (= last_seen fallback), hash_size (null), hash_size_inconsistent (false). Added schema version detection (v2 vs v3 observations table). Fixed QueryGroupedPackets first_seen. Added GetRecentTransmissionsForNode. All tests pass, build clean with Go 1.22.
|
||||||
- Fixed #133 (node count keeps climbing): `db.getStats().totalNodes` used `SELECT COUNT(*) FROM nodes` which counts every node ever seen — 6800+ on a ~200-400 node mesh. Changed `totalNodes` to count only nodes with `last_seen` within 7 days. Added `totalNodesAllTime` for the full historical count. Also filtered role counts in `/api/stats` to the same 7-day window. Added `countActiveNodes` and `countActiveNodesByRole` prepared statements in db.js. 6 new tests (95 total in test-db.js). The existing `idx_nodes_last_seen` index covers the new queries.
|
- Fixed #133 (node count keeps climbing): `db.getStats().totalNodes` used `SELECT COUNT(*) FROM nodes` which counts every node ever seen — 6800+ on a ~200-400 node mesh. Changed `totalNodes` to count only nodes with `last_seen` within 7 days. Added `totalNodesAllTime` for the full historical count. Also filtered role counts in `/api/stats` to the same 7-day window. Added `countActiveNodes` and `countActiveNodesByRole` prepared statements in db.js. 6 new tests (95 total in test-db.js). The existing `idx_nodes_last_seen` index covers the new queries.
|
||||||
- Go server FULL API parity: Rewrote QueryGroupedPackets from packets_v VIEW scan (8s on 1.2M rows) to transmission-centric query (<100ms). Fixed GetStats to use 7-day window for totalNodes + added totalNodesAllTime. Split GetRoleCounts into 7-day (for /api/stats) and all-time (for /api/nodes). Added packetsLastHour + node lat/lon/role to /api/observers via batch queries (GetObserverPacketCounts, GetNodeLocations). Added multi-node filter support (/api/packets?nodes=pk1,pk2). Fixed /api/packets/:id to return parsed path_json in path field. Populated bulk-health per-node stats from SQL. Updated test seed data to use dynamic timestamps for 7-day filter compatibility. All 42+ tests pass, go vet clean.
|
- Go server FULL API parity: Rewrote QueryGroupedPackets from packets_v VIEW scan (8s on 1.2M rows) to transmission-centric query (<100ms). Fixed GetStats to use 7-day window for totalNodes + added totalNodesAllTime. Split GetRoleCounts into 7-day (for /api/stats) and all-time (for /api/nodes). Added packetsLastHour + node lat/lon/role to /api/observers via batch queries (GetObserverPacketCounts, GetNodeLocations). Added multi-node filter support (/api/packets?nodes=pk1,pk2). Fixed /api/packets/:id to return parsed path_json in path field. Populated bulk-health per-node stats from SQL. Updated test seed data to use dynamic timestamps for 7-day filter compatibility. All 42+ tests pass, go vet clean.
|
||||||
- Fixed #133 ROOT CAUSE (phantom nodes): `autoLearnHopNodes` in server.js was calling `db.upsertNode()` for every unresolved hop prefix, creating thousands of fake "repeater" nodes with short public_keys (just the 2-4 byte hop prefix). Removed the `upsertNode` call entirely — unresolved hops are now simply cached to skip repeat DB lookups, and display as raw hex prefixes via hop-resolver. Added `db.removePhantomNodes()` that deletes nodes with `LENGTH(public_key) <= 16` (real pubkeys are 64 hex chars). Called at server startup to purge existing phantoms. 14 new test assertions (109 total in test-db.js).
|
- Fixed #133 ROOT CAUSE (phantom nodes): `autoLearnHopNodes` in server.js was calling `db.upsertNode()` for every unresolved hop prefix, creating thousands of fake "repeater" nodes with short public_keys (just the 2-4 byte hop prefix). Removed the `upsertNode` call entirely — unresolved hops are now simply cached to skip repeat DB lookups, and display as raw hex prefixes via hop-resolver. Added `db.removePhantomNodes()` that deletes nodes with `LENGTH(public_key) <= 16` (real pubkeys are 64 hex chars). Called at server startup to purge existing phantoms. 14 new test assertions (109 total in test-db.js).
|
||||||
- Fixed #126 (offline node showing on map due to hash prefix collision): `updatePathSeenTimestamps()` and `autoLearnHopNodes()` used `LIKE prefix%` DB queries that non-deterministically picked the first match when multiple nodes shared a hash prefix (e.g. `1CC4` and `1C82` both start with `1C` under 1-byte hash_size). Extracted `resolveUniquePrefixMatch()` that checks for uniqueness — ambiguous prefixes (matching 2+ nodes) are skipped and cached in a negative-cache Set. This prevents dead nodes from getting `last_heard` updates from packets that actually belong to a different node. 3 new tests (207 total in test-server-routes.js).
|
- Fixed #126 (offline node showing on map due to hash prefix collision): `updatePathSeenTimestamps()` and `autoLearnHopNodes()` used `LIKE prefix%` DB queries that non-deterministically picked the first match when multiple nodes shared a hash prefix (e.g. `1CC4` and `1C82` both start with `1C` under 1-byte hash_size). Extracted `resolveUniquePrefixMatch()` that checks for uniqueness — ambiguous prefixes (matching 2+ nodes) are skipped and cached in a negative-cache Set. This prevents dead nodes from getting `last_heard` updates from packets that actually belong to a different node. 3 new tests (207 total in test-server-routes.js).
|
||||||
- Fixed #123 (channel hash for undecrypted GRP_TXT): Added `channelHashHex` (zero-padded uppercase hex) and `decryptionStatus` ('decrypted'|'no_key'|'decryption_failed') fields to `decodeGrpTxt` in decoder.js. Distinguishes between "no channel keys configured" vs "keys tried but decryption failed." Frontend packets.js updated: list preview shows "🔒 Ch 0xXX (status)", detail pane hex breakdown and message area show channel hash with status label. 6 new tests (58 total in test-decoder.js).
|
- Fixed #123 (channel hash for undecrypted GRP_TXT): Added `channelHashHex` (zero-padded uppercase hex) and `decryptionStatus` ('decrypted'|'no_key'|'decryption_failed') fields to `decodeGrpTxt` in decoder.js. Distinguishes between "no channel keys configured" vs "keys tried but decryption failed." Frontend packets.js updated: list preview shows "🔒 Ch 0xXX (status)", detail pane hex breakdown and message area show channel hash with status label. 6 new tests (58 total in test-decoder.js).
|
||||||
- Ported in-memory packet store to Go (`cmd/server/store.go`). PacketStore loads all transmissions + observations from SQLite at startup via streaming query (no .all()), builds 5 indexes (byHash, byTxID, byObsID, byObserver, byNode), picks longest-path observation per transmission for display fields. QueryPackets and QueryGroupedPackets serve from memory with full filter support (type, route, observer, hash, since, until, region, node). Poller ingests new transmissions into store via IngestNewFromDB. Server/routes fall back to direct DB queries when store is nil (backward-compatible with tests). All 42+ existing tests pass, go vet clean, go build clean. System Go 1.17 requires using Go 1.22.5 at C:\go1.22\go\bin.
|
- Ported in-memory packet store to Go (`cmd/server/store.go`). PacketStore loads all transmissions + observations from SQLite at startup via streaming query (no .all()), builds 5 indexes (byHash, byTxID, byObsID, byObserver, byNode), picks longest-path observation per transmission for display fields. QueryPackets and QueryGroupedPackets serve from memory with full filter support (type, route, observer, hash, since, until, region, node). Poller ingests new transmissions into store via IngestNewFromDB. Server/routes fall back to direct DB queries when store is nil (backward-compatible with tests). All 42+ existing tests pass, go vet clean, go build clean. System Go 1.17 requires using Go 1.22.5 at C:\go1.22\go\bin.
|
||||||
- Fixed 3 critically slow Go endpoints by switching from SQLite queries against packets_v VIEW (1.2M rows) to in-memory PacketStore queries. `/api/channels` 7.2s→37ms (195×), `/api/channels/:hash/messages` 8.2s→36ms (228×), `/api/analytics/rf` 4.2s→90ms avg (47×). Key optimizations: (1) byPayloadType index reduces channels scan from 52K to 17K packets, (2) struct-based JSON decode avoids map[string]interface{} allocations, (3) per-transmission work hoisted out of 1.2M observation loop for RF, (4) eliminated second-pass time.Parse over 1.2M observations (track min/max timestamps as strings instead), (5) pre-allocated slices with capacity hints, (6) 15-second TTL cache for RF analytics (separate mutex to avoid contention with store RWMutex). Cache invalidation is TTL-only because live mesh generates continuous ingest events. Also fixed `/api/analytics/channels` to use store. All handlers fall back to DB when store is nil (test compat).
|
- Fixed 3 critically slow Go endpoints by switching from SQLite queries against packets_v VIEW (1.2M rows) to in-memory PacketStore queries. `/api/channels` 7.2s→37ms (195×), `/api/channels/:hash/messages` 8.2s→36ms (228×), `/api/analytics/rf` 4.2s→90ms avg (47×). Key optimizations: (1) byPayloadType index reduces channels scan from 52K to 17K packets, (2) struct-based JSON decode avoids map[string]interface{} allocations, (3) per-transmission work hoisted out of 1.2M observation loop for RF, (4) eliminated second-pass time.Parse over 1.2M observations (track min/max timestamps as strings instead), (5) pre-allocated slices with capacity hints, (6) 15-second TTL cache for RF analytics (separate mutex to avoid contention with store RWMutex). Cache invalidation is TTL-only because live mesh generates continuous ingest events. Also fixed `/api/analytics/channels` to use store. All handlers fall back to DB when store is nil (test compat).
|
||||||
- **Massive session 2026-03-27 (FULL DAY):** Delivered 6 critical fixes + Go rewrite completed:
|
- **Massive session 2026-03-27 (FULL DAY):** Delivered 6 critical fixes + Go rewrite completed:
|
||||||
- **#133 PHANTOM NODES (ROOT CAUSE):** Backend `autoLearnHopNodes()` removed upsertNode call. Added `db.removePhantomNodes()` (pubkey ≤16 chars). Called at startup. Cascadia: 7,308 → ~200-400 active nodes. 14 new tests, all passing.
|
- **#133 PHANTOM NODES (ROOT CAUSE):** Backend `autoLearnHopNodes()` removed upsertNode call. Added `db.removePhantomNodes()` (pubkey ≤16 chars). Called at startup. Cascadia: 7,308 → ~200-400 active nodes. 14 new tests, all passing.
|
||||||
- **#133 ACTIVE WINDOW:** `/api/stats` `totalNodes` now 7-day window. Added `totalNodesAllTime` for historical. Role counts filtered to 7-day. Go server GetStats updated for parity.
|
- **#133 ACTIVE WINDOW:** `/api/stats` `totalNodes` now 7-day window. Added `totalNodesAllTime` for historical. Role counts filtered to 7-day. Go server GetStats updated for parity.
|
||||||
- **#126 AMBIGUOUS PREFIXES:** `resolveUniquePrefixMatch()` requires unique prefix match. Ambiguous prefixes skipped, cached in negative-cache. Prevents dead nodes from wrong packet attribution.
|
- **#126 AMBIGUOUS PREFIXES:** `resolveUniquePrefixMatch()` requires unique prefix match. Ambiguous prefixes skipped, cached in negative-cache. Prevents dead nodes from wrong packet attribution.
|
||||||
- **#123 CHANNEL HASH:** Decoder tracks `channelHashHex` + `decryptionStatus` ('decrypted'|'no_key'|'decryption_failed'). All 4 fixes tested, deployed.
|
- **#123 CHANNEL HASH:** Decoder tracks `channelHashHex` + `decryptionStatus` ('decrypted'|'no_key'|'decryption_failed'). All 4 fixes tested, deployed.
|
||||||
- **Go API Parity:** QueryGroupedPackets transmission-centric 8s→<100ms. Response shapes match Node.js exactly. All 42+ Go tests passing.
|
- **Go API Parity:** QueryGroupedPackets transmission-centric 8s→<100ms. Response shapes match Node.js exactly. All 42+ Go tests passing.
|
||||||
- **Database merge:** Staging 185MB (50K tx + 1.2M obs) merged into prod 21MB. 0 data loss. Merged DB 51,723 tx + 1,237,186 obs. Deploy time 8,491ms, memory 860MiB RSS (v.s. 2.7GB pre-RAM-fix). Backups retained 7 days.
|
- **Database merge:** Staging 185MB (50K tx + 1.2M obs) merged into prod 21MB. 0 data loss. Merged DB 51,723 tx + 1,237,186 obs. Deploy time 8,491ms, memory 860MiB RSS (v.s. 2.7GB pre-RAM-fix). Backups retained 7 days.
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
# Hudson — DevOps Engineer
|
# Hudson — DevOps Engineer
|
||||||
|
|
||||||
## Identity
|
## Identity
|
||||||
- **Name:** Hudson
|
- **Name:** Hudson
|
||||||
- **Role:** DevOps Engineer
|
- **Role:** DevOps Engineer
|
||||||
- **Emoji:** ⚙️
|
- **Emoji:** ⚙️
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
- CI/CD pipeline (`.github/workflows/deploy.yml`)
|
- CI/CD pipeline (`.github/workflows/deploy.yml`)
|
||||||
- Docker configuration (`Dockerfile`, `docker/`)
|
- Docker configuration (`Dockerfile`, `docker/`)
|
||||||
- Deployment scripts (`manage.sh`)
|
- Deployment scripts (`manage.sh`)
|
||||||
- Production infrastructure and monitoring
|
- Production infrastructure and monitoring
|
||||||
- Server configuration and environment setup
|
- Server configuration and environment setup
|
||||||
- Performance profiling and optimization of CI/build pipelines
|
- Performance profiling and optimization of CI/build pipelines
|
||||||
- Database operations (backup, recovery, migration)
|
- Database operations (backup, recovery, migration)
|
||||||
- Coverage collection pipeline (`scripts/collect-frontend-coverage.js`)
|
- Coverage collection pipeline (`scripts/collect-frontend-coverage.js`)
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
- Does NOT write application features — that's Hicks (backend) and Newt (frontend)
|
- Does NOT write application features — that's Hicks (backend) and Newt (frontend)
|
||||||
- Does NOT write application tests — that's Bishop
|
- Does NOT write application tests — that's Bishop
|
||||||
- MAY modify test infrastructure (CI config, coverage tooling, test runners)
|
- MAY modify test infrastructure (CI config, coverage tooling, test runners)
|
||||||
- MAY modify server startup/config for deployment purposes
|
- MAY modify server startup/config for deployment purposes
|
||||||
- Coordinates with Kobayashi on infrastructure decisions
|
- Coordinates with Kobayashi on infrastructure decisions
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
- `.github/workflows/deploy.yml` — CI/CD pipeline
|
- `.github/workflows/deploy.yml` — CI/CD pipeline
|
||||||
- `Dockerfile`, `docker/` — Container config
|
- `Dockerfile`, `docker/` — Container config
|
||||||
- `manage.sh` — Deployment management script
|
- `manage.sh` — Deployment management script
|
||||||
- `scripts/` — Build and coverage scripts
|
- `scripts/` — Build and coverage scripts
|
||||||
- `config.example.json` — Configuration template
|
- `config.example.json` — Configuration template
|
||||||
- `package.json` — Dependencies and scripts
|
- `package.json` — Dependencies and scripts
|
||||||
|
|
||||||
## Principles
|
## Principles
|
||||||
- Infrastructure as code — all config in version control
|
- Infrastructure as code — all config in version control
|
||||||
- CI must stay under 10 minutes (currently ~14min — fix this)
|
- CI must stay under 10 minutes (currently ~14min — fix this)
|
||||||
- Never break the deploy pipeline
|
- Never break the deploy pipeline
|
||||||
- Test infrastructure changes locally before pushing
|
- Test infrastructure changes locally before pushing
|
||||||
- Read AGENTS.md before any work
|
- Read AGENTS.md before any work
|
||||||
|
|
||||||
## Model
|
## Model
|
||||||
Preferred: auto
|
Preferred: auto
|
||||||
|
|||||||
@@ -84,5 +84,5 @@ Historical context from earlier phases:
|
|||||||
- Only Hudson touches prod infrastructure (user directive)
|
- Only Hudson touches prod infrastructure (user directive)
|
||||||
- Go staging runs on port 82 (future phase)
|
- Go staging runs on port 82 (future phase)
|
||||||
- Backups retained 7 days post-merge
|
- Backups retained 7 days post-merge
|
||||||
- Manual promotion flow (no auto-promotion to prod)
|
- Manual promotion flow (no auto-promotion to prod)
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
# Kobayashi — Lead
|
# Kobayashi — Lead
|
||||||
|
|
||||||
Architecture, code review, and decision-making for CoreScope.
|
Architecture, code review, and decision-making for CoreScope.
|
||||||
|
|
||||||
## Project Context
|
## Project Context
|
||||||
|
|
||||||
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
|
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
|
||||||
**Stack:** Node.js 18+, Express 5, SQLite, vanilla JS frontend, Leaflet, WebSocket, MQTT
|
**Stack:** Node.js 18+, Express 5, SQLite, vanilla JS frontend, Leaflet, WebSocket, MQTT
|
||||||
**User:** User
|
**User:** User
|
||||||
|
|
||||||
## Responsibilities
|
## Responsibilities
|
||||||
|
|
||||||
- Review architecture decisions and feature proposals
|
- Review architecture decisions and feature proposals
|
||||||
- Code review — approve or reject with actionable feedback
|
- Code review — approve or reject with actionable feedback
|
||||||
- Scope decisions — what to build, what to defer
|
- Scope decisions — what to build, what to defer
|
||||||
- Documentation updates (README, docs/)
|
- Documentation updates (README, docs/)
|
||||||
- Ensure AGENTS.md rules are followed (plan before implementing, tests required, cache busters, etc.)
|
- Ensure AGENTS.md rules are followed (plan before implementing, tests required, cache busters, etc.)
|
||||||
- Coordinate multi-domain changes spanning backend and frontend
|
- Coordinate multi-domain changes spanning backend and frontend
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
- Do NOT write implementation code — delegate to Hicks (backend) or Newt (frontend)
|
- Do NOT write implementation code — delegate to Hicks (backend) or Newt (frontend)
|
||||||
- May write small fixes during code review if the change is trivial
|
- May write small fixes during code review if the change is trivial
|
||||||
- Architecture proposals require user sign-off before implementation starts
|
- Architecture proposals require user sign-off before implementation starts
|
||||||
|
|
||||||
## Review Authority
|
## Review Authority
|
||||||
|
|
||||||
- May approve or reject work from Hicks, Newt, and Bishop
|
- May approve or reject work from Hicks, Newt, and Bishop
|
||||||
- On rejection: specify whether to reassign or escalate
|
- On rejection: specify whether to reassign or escalate
|
||||||
- Lockout rules apply — rejected author cannot self-revise
|
- Lockout rules apply — rejected author cannot self-revise
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
- AGENTS.md — project rules (read before every review)
|
- AGENTS.md — project rules (read before every review)
|
||||||
- server.js — main backend (2,661 lines)
|
- server.js — main backend (2,661 lines)
|
||||||
- public/ — frontend modules (22 files)
|
- public/ — frontend modules (22 files)
|
||||||
- package.json — dependencies (keep minimal)
|
- package.json — dependencies (keep minimal)
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
# Kobayashi — History
|
# Kobayashi — History
|
||||||
|
|
||||||
## Project Context
|
## Project Context
|
||||||
|
|
||||||
CoreScope is a real-time LoRa mesh packet analyzer. Node.js + Express + SQLite backend, vanilla JS SPA frontend with Leaflet maps, WebSocket live feed, MQTT ingestion. Production at v2.6.0, ~18K lines, 85%+ backend test coverage.
|
CoreScope is a real-time LoRa mesh packet analyzer. Node.js + Express + SQLite backend, vanilla JS SPA frontend with Leaflet maps, WebSocket live feed, MQTT ingestion. Production at v2.6.0, ~18K lines, 85%+ backend test coverage.
|
||||||
|
|
||||||
User: User
|
User: User
|
||||||
|
|
||||||
## Learnings
|
## Learnings
|
||||||
|
|
||||||
- Session started 2026-03-26. Team formed: Kobayashi (Lead), Hicks (Backend), Newt (Frontend), Bishop (Tester).
|
- Session started 2026-03-26. Team formed: Kobayashi (Lead), Hicks (Backend), Newt (Frontend), Bishop (Tester).
|
||||||
- **E2E Playwright performance audit (2026-03-26):** 16 tests, single browser/context/page (good). Key bottlenecks: (1) `waitUntil: 'networkidle'` used ~20 times — catastrophic for SPA with WebSocket + map tiles, (2) ~17s of hardcoded `waitForTimeout` sleeps, (3) redundant `page.goto()` to same routes across tests, (4) CI installs Playwright browser on every run with no caching, (5) coverage collection launches a second full browser session, (6) `sleep 5` server startup instead of health-check polling. Estimated 40-50% total runtime reduction achievable.
|
- **E2E Playwright performance audit (2026-03-26):** 16 tests, single browser/context/page (good). Key bottlenecks: (1) `waitUntil: 'networkidle'` used ~20 times — catastrophic for SPA with WebSocket + map tiles, (2) ~17s of hardcoded `waitForTimeout` sleeps, (3) redundant `page.goto()` to same routes across tests, (4) CI installs Playwright browser on every run with no caching, (5) coverage collection launches a second full browser session, (6) `sleep 5` server startup instead of health-check polling. Estimated 40-50% total runtime reduction achievable.
|
||||||
- **Issue triage session (2026-03-27):** Triaged 4 open issues, assigned to team:
|
- **Issue triage session (2026-03-27):** Triaged 4 open issues, assigned to team:
|
||||||
- **#131** (Feature: Auto-update nodes tab) → Newt (⚛️). Requires WebSocket real-time updates in nodes.js, similar to existing packets feed.
|
- **#131** (Feature: Auto-update nodes tab) → Newt (⚛️). Requires WebSocket real-time updates in nodes.js, similar to existing packets feed.
|
||||||
- **#130** (Bug: Disappearing nodes on live map) → Newt (⚛️). High severity, multiple Cascadia Mesh community reports. Likely status calculation or map filter bug. Nodes visible in static list but vanishing from live map.
|
- **#130** (Bug: Disappearing nodes on live map) → Newt (⚛️). High severity, multiple Cascadia Mesh community reports. Likely status calculation or map filter bug. Nodes visible in static list but vanishing from live map.
|
||||||
- **#129** (Feature: Packet comparison between observers) → Newt (⚛️). Feature request from letsmesh analyzer. Side-by-side packet filtering for two repeaters to diagnose repeater issues.
|
- **#129** (Feature: Packet comparison between observers) → Newt (⚛️). Feature request from letsmesh analyzer. Side-by-side packet filtering for two repeaters to diagnose repeater issues.
|
||||||
- **#123** (Feature: Show channel hash on decrypt failure) → Hicks (🔧). Core contributor (lincomatic) request. Decoder needs to track why decrypt failed (no key vs. corruption) and expose channel hash + reason in API response.
|
- **#123** (Feature: Show channel hash on decrypt failure) → Hicks (🔧). Core contributor (lincomatic) request. Decoder needs to track why decrypt failed (no key vs. corruption) and expose channel hash + reason in API response.
|
||||||
- **Massive session — 2026-03-27 (full day):**
|
- **Massive session — 2026-03-27 (full day):**
|
||||||
- **#133 root cause (phantom nodes):** `autoLearnHopNodes()` creates stub nodes for unresolved hop prefixes (2-8 hex chars). Cascadia showed 7,308 nodes (6,638 repeaters) when real size ~200-400. With `hash_size=1`, collision rate high → infinite phantom generation.
|
- **#133 root cause (phantom nodes):** `autoLearnHopNodes()` creates stub nodes for unresolved hop prefixes (2-8 hex chars). Cascadia showed 7,308 nodes (6,638 repeaters) when real size ~200-400. With `hash_size=1`, collision rate high → infinite phantom generation.
|
||||||
- **DB merge decision:** Staging DB (185MB, 50K transmissions, 1.2M observations) is superset. Use as merge base. Transmissions dedup by hash (unique), observations all preserved (unique by observer), nodes/observers latest-wins + sum counts. 6-phase execution plan: pre-flight, backup, merge, deploy, validate, cleanup.
|
- **DB merge decision:** Staging DB (185MB, 50K transmissions, 1.2M observations) is superset. Use as merge base. Transmissions dedup by hash (unique), observations all preserved (unique by observer), nodes/observers latest-wins + sum counts. 6-phase execution plan: pre-flight, backup, merge, deploy, validate, cleanup.
|
||||||
- **Coordination:** Assigned Hicks phantom cleanup (backend), Newt live page pruning (frontend), Hudson merge execution (DevOps).
|
- **Coordination:** Assigned Hicks phantom cleanup (backend), Newt live page pruning (frontend), Hudson merge execution (DevOps).
|
||||||
- **Outcome:** All 4 triaged issues fixed (#131, #130, #129, #123), #133 (phantom nodes) fully resolved, #126 (ambiguous hop prefixes) fixed as bonus, database merged successfully (0 data loss, 2 min downtime, 51,723 tx + 1.237M obs), Go rewrite (MQTT ingestor + web server) completed and ready for staging.
|
- **Outcome:** All 4 triaged issues fixed (#131, #130, #129, #123), #133 (phantom nodes) fully resolved, #126 (ambiguous hop prefixes) fixed as bonus, database merged successfully (0 data loss, 2 min downtime, 51,723 tx + 1.237M obs), Go rewrite (MQTT ingestor + web server) completed and ready for staging.
|
||||||
- **Team expanded:** Hudson joined for DevOps work, Ripley joined as Support Engineer.
|
- **Team expanded:** Hudson joined for DevOps work, Ripley joined as Support Engineer.
|
||||||
- **Go staging bug triage (2026-03-28):** Filed 8 issues for Go staging bugs missed during API parity work. All found by actually loading the analytics page in a browser — none caught by endpoint-level parity checks.
|
- **Go staging bug triage (2026-03-28):** Filed 8 issues for Go staging bugs missed during API parity work. All found by actually loading the analytics page in a browser — none caught by endpoint-level parity checks.
|
||||||
- **#142** (Channels tab: wrong count, all decrypted, undefined fields) → Hicks
|
- **#142** (Channels tab: wrong count, all decrypted, undefined fields) → Hicks
|
||||||
- **#136** (Hash stats tab: empty) → Hicks
|
- **#136** (Hash stats tab: empty) → Hicks
|
||||||
- **#138** (Hash issues: no inconsistencies/collision risks shown) → Hicks
|
- **#138** (Hash issues: no inconsistencies/collision risks shown) → Hicks
|
||||||
- **#135** (Topology tab: broken) → Hicks
|
- **#135** (Topology tab: broken) → Hicks
|
||||||
- **#134** (Route patterns: broken) → Hicks
|
- **#134** (Route patterns: broken) → Hicks
|
||||||
- **#140** (bulk-health API: 12s response time) → Hicks
|
- **#140** (bulk-health API: 12s response time) → Hicks
|
||||||
- **#137** (Distance tab: broken) → Hicks
|
- **#137** (Distance tab: broken) → Hicks
|
||||||
- **#139** (Commit link: bad contrast) → Newt
|
- **#139** (Commit link: bad contrast) → Newt
|
||||||
- **Post-mortem:** Parity was verified by comparing individual endpoint response shapes in isolation. Nobody loaded the analytics page in a browser and looked at it. The agents tested API responses without browser validation of the full UI — exactly the failure mode AGENTS.md rule #2 exists to prevent.
|
- **Post-mortem:** Parity was verified by comparing individual endpoint response shapes in isolation. Nobody loaded the analytics page in a browser and looked at it. The agents tested API responses without browser validation of the full UI — exactly the failure mode AGENTS.md rule #2 exists to prevent.
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
# Newt — Frontend Dev
|
# Newt — Frontend Dev
|
||||||
|
|
||||||
Vanilla JS UI, Leaflet maps, live visualization, theming, and all public/ modules for CoreScope.
|
Vanilla JS UI, Leaflet maps, live visualization, theming, and all public/ modules for CoreScope.
|
||||||
|
|
||||||
## Project Context
|
## Project Context
|
||||||
|
|
||||||
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
|
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
|
||||||
**Stack:** Vanilla HTML/CSS/JavaScript (ES5/6), Leaflet maps, WebSocket, Canvas animations
|
**Stack:** Vanilla HTML/CSS/JavaScript (ES5/6), Leaflet maps, WebSocket, Canvas animations
|
||||||
**User:** User
|
**User:** User
|
||||||
|
|
||||||
## Responsibilities
|
## Responsibilities
|
||||||
|
|
||||||
- public/*.js — All 22 frontend modules (app.js, packets.js, live.js, map.js, nodes.js, channels.js, analytics.js, customize.js, etc.)
|
- public/*.js — All 22 frontend modules (app.js, packets.js, live.js, map.js, nodes.js, channels.js, analytics.js, customize.js, etc.)
|
||||||
- public/style.css, public/live.css, public/home.css — Styling via CSS variables
|
- public/style.css, public/live.css, public/home.css — Styling via CSS variables
|
||||||
- public/index.html — SPA shell, cache busters (MUST bump on every .js/.css change)
|
- public/index.html — SPA shell, cache busters (MUST bump on every .js/.css change)
|
||||||
- packet-filter.js — Wireshark-style filter engine (standalone, testable in Node.js)
|
- packet-filter.js — Wireshark-style filter engine (standalone, testable in Node.js)
|
||||||
- Leaflet map rendering, VCR playback controls, Canvas animations
|
- Leaflet map rendering, VCR playback controls, Canvas animations
|
||||||
- Theme customizer (IIFE in customize.js, THEME_CSS_MAP)
|
- Theme customizer (IIFE in customize.js, THEME_CSS_MAP)
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
- Do NOT modify server-side files (server.js, db.js, packet-store.js, decoder.js)
|
- Do NOT modify server-side files (server.js, db.js, packet-store.js, decoder.js)
|
||||||
- All colors MUST use CSS variables — never hardcode #hex outside :root
|
- All colors MUST use CSS variables — never hardcode #hex outside :root
|
||||||
- Use shared helpers from roles.js (ROLE_COLORS, TYPE_COLORS, getNodeStatus, getHealthThresholds)
|
- Use shared helpers from roles.js (ROLE_COLORS, TYPE_COLORS, getNodeStatus, getHealthThresholds)
|
||||||
- Prefer `n.last_heard || n.last_seen` for display and status
|
- Prefer `n.last_heard || n.last_seen` for display and status
|
||||||
- No per-packet API calls from frontend — fetch bulk, filter client-side
|
- No per-packet API calls from frontend — fetch bulk, filter client-side
|
||||||
- Run `node test-packet-filter.js` and `node test-frontend-helpers.js` after filter/helper changes
|
- Run `node test-packet-filter.js` and `node test-frontend-helpers.js` after filter/helper changes
|
||||||
- Always bump cache busters in the SAME commit as code changes
|
- Always bump cache busters in the SAME commit as code changes
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
- live.js (2,178 lines) — largest frontend module, VCR playback
|
- live.js (2,178 lines) — largest frontend module, VCR playback
|
||||||
- analytics.js (1,375 lines) — global analytics dashboard
|
- analytics.js (1,375 lines) — global analytics dashboard
|
||||||
- customize.js (1,259 lines) — theme customizer IIFE
|
- customize.js (1,259 lines) — theme customizer IIFE
|
||||||
- packets.js (1,669 lines) — packet feed, detail pane, hex breakdown
|
- packets.js (1,669 lines) — packet feed, detail pane, hex breakdown
|
||||||
- app.js (775 lines) — SPA router, WebSocket, globals
|
- app.js (775 lines) — SPA router, WebSocket, globals
|
||||||
- nodes.js (765 lines) — node directory, detail views
|
- nodes.js (765 lines) — node directory, detail views
|
||||||
- map.js (699 lines) — Leaflet map rendering
|
- map.js (699 lines) — Leaflet map rendering
|
||||||
- packet-filter.js — standalone filter engine
|
- packet-filter.js — standalone filter engine
|
||||||
- roles.js — shared color maps and helpers
|
- roles.js — shared color maps and helpers
|
||||||
- hop-resolver.js — client-side hop resolution
|
- hop-resolver.js — client-side hop resolution
|
||||||
|
|
||||||
## Model
|
## Model
|
||||||
|
|
||||||
Preferred: auto
|
Preferred: auto
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
# Newt — History
|
# Newt — History
|
||||||
|
|
||||||
## Project Context
|
## Project Context
|
||||||
|
|
||||||
CoreScope is a real-time LoRa mesh packet analyzer with a vanilla JS SPA frontend. 22 frontend modules, Leaflet maps, WebSocket live feed, VCR playback, Canvas animations, theme customizer with CSS variables. No build step, no framework. ES5/6 for broad browser support.
|
CoreScope is a real-time LoRa mesh packet analyzer with a vanilla JS SPA frontend. 22 frontend modules, Leaflet maps, WebSocket live feed, VCR playback, Canvas animations, theme customizer with CSS variables. No build step, no framework. ES5/6 for broad browser support.
|
||||||
|
|
||||||
User: User
|
User: User
|
||||||
|
|
||||||
## Learnings
|
## Learnings
|
||||||
|
|
||||||
- Session started 2026-03-26. Team formed: Kobayashi (Lead), Hicks (Backend), Newt (Frontend), Bishop (Tester).
|
- Session started 2026-03-26. Team formed: Kobayashi (Lead), Hicks (Backend), Newt (Frontend), Bishop (Tester).
|
||||||
- **Issue #127 fix:** Firefox clipboard API fails silently when `navigator.clipboard.writeText()` is called outside a secure context or without proper user gesture handling. Added `window.copyToClipboard()` shared helper to `roles.js` that tries Clipboard API first, falls back to hidden textarea + `document.execCommand('copy')`. Updated all 3 clipboard call sites: `nodes.js` (Copy URL — the reported bug), `packets.js` (Copy Link — had ugly `prompt()` fallback), `customize.js` (Copy to Clipboard — already worked but now uses shared helper). Cache busters bumped. All tests pass (47 frontend, 62 packet-filter).
|
- **Issue #127 fix:** Firefox clipboard API fails silently when `navigator.clipboard.writeText()` is called outside a secure context or without proper user gesture handling. Added `window.copyToClipboard()` shared helper to `roles.js` that tries Clipboard API first, falls back to hidden textarea + `document.execCommand('copy')`. Updated all 3 clipboard call sites: `nodes.js` (Copy URL — the reported bug), `packets.js` (Copy Link — had ugly `prompt()` fallback), `customize.js` (Copy to Clipboard — already worked but now uses shared helper). Cache busters bumped. All tests pass (47 frontend, 62 packet-filter).
|
||||||
- **Issue #125 fix:** Added dismiss/close button (✕) to the packet detail pane on desktop. Extracted `closeDetailPanel()` shared helper and `PANEL_CLOSE_HTML` constant — DRY: Escape handler and click handler both call it. Close button uses event delegation on `#pktRight`, styled with CSS variables (`--text-muted`, `--text`, `--surface-1`) matching the mobile `.mobile-sheet-close` pattern. Hidden when panel is in `.empty` state. Clicking a different row still re-opens with new data. Files changed: `public/packets.js`, `public/style.css`. Cache busters NOT bumped (another agent editing index.html).
|
- **Issue #125 fix:** Added dismiss/close button (✕) to the packet detail pane on desktop. Extracted `closeDetailPanel()` shared helper and `PANEL_CLOSE_HTML` constant — DRY: Escape handler and click handler both call it. Close button uses event delegation on `#pktRight`, styled with CSS variables (`--text-muted`, `--text`, `--surface-1`) matching the mobile `.mobile-sheet-close` pattern. Hidden when panel is in `.empty` state. Clicking a different row still re-opens with new data. Files changed: `public/packets.js`, `public/style.css`. Cache busters NOT bumped (another agent editing index.html).
|
||||||
- **Issue #122 fix:** Node tooltip (line 45) and node detail panel (line 120) in `channels.js` used `last_seen` alone for "Last seen" display. Changed both to `last_heard || last_seen` per AGENTS.md pitfall. Pattern: always prefer `last_heard || last_seen` for any time-ago display. **Server note for Hicks:** `/api/nodes/search` and `/api/nodes/:pubkey` endpoints don't return `last_heard` — only the bulk `/api/nodes` list endpoint computes it from the in-memory packet store. These endpoints need the same `last_heard` enrichment for the frontend fix to fully take effect. Also, `/api/analytics/channels` has a separate bug: `lastActivity` is overwritten unconditionally (no `>=` check) so it shows the oldest packet's timestamp, not the newest.
|
- **Issue #122 fix:** Node tooltip (line 45) and node detail panel (line 120) in `channels.js` used `last_seen` alone for "Last seen" display. Changed both to `last_heard || last_seen` per AGENTS.md pitfall. Pattern: always prefer `last_heard || last_seen` for any time-ago display. **Server note for Hicks:** `/api/nodes/search` and `/api/nodes/:pubkey` endpoints don't return `last_heard` — only the bulk `/api/nodes` list endpoint computes it from the in-memory packet store. These endpoints need the same `last_heard` enrichment for the frontend fix to fully take effect. Also, `/api/analytics/channels` has a separate bug: `lastActivity` is overwritten unconditionally (no `>=` check) so it shows the oldest packet's timestamp, not the newest.
|
||||||
- **Issue #130 fix:** Live map `pruneStaleNodes()` (added for #133) was completely removing stale nodes from the map, while the static map dims them with CSS. Root cause: API-loaded nodes and WS-only nodes were treated identically — both got deleted when stale. Fix: mark API-loaded nodes with `_fromAPI = true` in `loadNodes()`. `pruneStaleNodes()` now dims API nodes (fillOpacity 0.25, opacity 0.15) instead of removing them, and restores full opacity when they become active again. WS-only dynamic nodes are still removed to prevent memory leaks. Pattern: **live map should match static map behavior** — never remove database-loaded nodes, only change their visual state. 3 new tests added (63 total frontend tests passing).
|
- **Issue #130 fix:** Live map `pruneStaleNodes()` (added for #133) was completely removing stale nodes from the map, while the static map dims them with CSS. Root cause: API-loaded nodes and WS-only nodes were treated identically — both got deleted when stale. Fix: mark API-loaded nodes with `_fromAPI = true` in `loadNodes()`. `pruneStaleNodes()` now dims API nodes (fillOpacity 0.25, opacity 0.15) instead of removing them, and restores full opacity when they become active again. WS-only dynamic nodes are still removed to prevent memory leaks. Pattern: **live map should match static map behavior** — never remove database-loaded nodes, only change their visual state. 3 new tests added (63 total frontend tests passing).
|
||||||
- **Issue #129 fix:** Added observer packet comparison feature (`#/compare` page). Users select two observers from dropdowns, click Compare, and see which packets each observer saw in the last 24 hours. Data flow: fetches packets per observer via existing `/api/packets?observer=X&limit=10000&since=24h`, computes set intersection/difference client-side using `comparePacketSets()` (O(n) via Set lookups — no nested loops). UI: three summary cards (both/only-A/only-B with counts and percentages), horizontal stacked bar chart, packet type breakdown for shared packets, and tabbed detail tables (up to 200 rows each, clickable to packet detail). URL is shareable: `#/compare?a=ID1&b=ID2`. Added 🔍 compare button to observers page header. Pure function `comparePacketSets` exposed on `window` for testability. 11 new tests (87 total frontend tests). Files: `public/compare.js` (new), `public/style.css`, `public/observers.js`, `public/index.html`, `test-frontend-helpers.js`. Cache busters bumped.
|
- **Issue #129 fix:** Added observer packet comparison feature (`#/compare` page). Users select two observers from dropdowns, click Compare, and see which packets each observer saw in the last 24 hours. Data flow: fetches packets per observer via existing `/api/packets?observer=X&limit=10000&since=24h`, computes set intersection/difference client-side using `comparePacketSets()` (O(n) via Set lookups — no nested loops). UI: three summary cards (both/only-A/only-B with counts and percentages), horizontal stacked bar chart, packet type breakdown for shared packets, and tabbed detail tables (up to 200 rows each, clickable to packet detail). URL is shareable: `#/compare?a=ID1&b=ID2`. Added 🔍 compare button to observers page header. Pure function `comparePacketSets` exposed on `window` for testability. 11 new tests (87 total frontend tests). Files: `public/compare.js` (new), `public/style.css`, `public/observers.js`, `public/index.html`, `test-frontend-helpers.js`. Cache busters bumped.
|
||||||
- **Browser validation of 6 fixes (2026-03-27):** Validated against live prod at `https://analyzer.00id.net`. Results: ✅ #133 (phantom nodes) — API returns 50 nodes, reasonable count, no runaway growth. ✅ #123 (channel hash on undecrypted) — GRP_TXT packets with `decryption_failed` status show `channelHashHex` field; packet detail renders `🔒 Channel Hash: 0xE2 (decryption failed)` via `packets.js:1254-1259`. ⏭ #126 (offline node on map) — skipped, requires specific dead node. ✅ #130 (disappearing nodes on live map) — `pruneStaleNodes()` confirmed at `live.js:1474` dims API-loaded nodes (`fillOpacity:0.25`) instead of removing; `_fromAPI=true` flag set at `live.js:1279`. ✅ #131 (auto-updating node list) — `nodes.js:210-216` wires `debouncedOnWS` handler that triggers `loadNodes(true)` on ADVERT messages; `isAdvertMessage()` at `nodes.js:852` checks `payload_type===4`. ✅ #129 (observer comparison) — `compare.js` deployed with full UI: observer dropdowns, `comparePacketSets()` Set logic, summary cards, bar chart, type breakdown. 16 observers available in prod. Pattern: always verify deployed JS matches source — cache buster `v=1774625000` confirmed consistent across all script tags.
|
- **Browser validation of 6 fixes (2026-03-27):** Validated against live prod at `https://analyzer.00id.net`. Results: ✅ #133 (phantom nodes) — API returns 50 nodes, reasonable count, no runaway growth. ✅ #123 (channel hash on undecrypted) — GRP_TXT packets with `decryption_failed` status show `channelHashHex` field; packet detail renders `🔒 Channel Hash: 0xE2 (decryption failed)` via `packets.js:1254-1259`. ⏭ #126 (offline node on map) — skipped, requires specific dead node. ✅ #130 (disappearing nodes on live map) — `pruneStaleNodes()` confirmed at `live.js:1474` dims API-loaded nodes (`fillOpacity:0.25`) instead of removing; `_fromAPI=true` flag set at `live.js:1279`. ✅ #131 (auto-updating node list) — `nodes.js:210-216` wires `debouncedOnWS` handler that triggers `loadNodes(true)` on ADVERT messages; `isAdvertMessage()` at `nodes.js:852` checks `payload_type===4`. ✅ #129 (observer comparison) — `compare.js` deployed with full UI: observer dropdowns, `comparePacketSets()` Set logic, summary cards, bar chart, type breakdown. 16 observers available in prod. Pattern: always verify deployed JS matches source — cache buster `v=1774625000` confirmed consistent across all script tags.
|
||||||
- **Packet detail pane fresh-load fix:** The `detail-collapsed` class added for issue #125's close button wasn't applied on initial render, so the empty right panel was visible on fresh page load. Fix: added `detail-collapsed` to the `split-layout` div in the initial `innerHTML` template (packets.js:183). Pattern: when adding a CSS toggle class, always consider the initial DOM state — if nothing is selected, the default state must match "nothing selected." 3 tests added (90 total frontend). Cache busters bumped.
|
- **Packet detail pane fresh-load fix:** The `detail-collapsed` class added for issue #125's close button wasn't applied on initial render, so the empty right panel was visible on fresh page load. Fix: added `detail-collapsed` to the `split-layout` div in the initial `innerHTML` template (packets.js:183). Pattern: when adding a CSS toggle class, always consider the initial DOM state — if nothing is selected, the default state must match "nothing selected." 3 tests added (90 total frontend). Cache busters bumped.
|
||||||
- **Massive session 2026-03-27 (FULL DAY):** Delivered 4 critical frontend fixes + live page improvements:
|
- **Massive session 2026-03-27 (FULL DAY):** Delivered 4 critical frontend fixes + live page improvements:
|
||||||
- **#130 LIVE MAP STALE DIMMING:** `pruneStaleNodes()` distinguishes API-loaded (`_fromAPI`) from WS-only. Dims API nodes (fillOpacity 0.25, opacity 0.15) instead of removing. Matches static map behavior. 3 new tests, all passing.
|
- **#130 LIVE MAP STALE DIMMING:** `pruneStaleNodes()` distinguishes API-loaded (`_fromAPI`) from WS-only. Dims API nodes (fillOpacity 0.25, opacity 0.15) instead of removing. Matches static map behavior. 3 new tests, all passing.
|
||||||
- **#131 NODES TAB WS AUTO-UPDATE:** `loadNodes(refreshOnly)` pattern resets cache + invalidateApiCache + re-fetches. Preserves scroll/selection/listeners. WS handler now triggers on ADVERT messages (payload_type===4). All tests passing.
|
- **#131 NODES TAB WS AUTO-UPDATE:** `loadNodes(refreshOnly)` pattern resets cache + invalidateApiCache + re-fetches. Preserves scroll/selection/listeners. WS handler now triggers on ADVERT messages (payload_type===4). All tests passing.
|
||||||
- **#129 OBSERVER COMPARISON PAGE:** New `#/compare` route with shareable params `?a=ID1&b=ID2`. `comparePacketSets()` pure function (O(n) Set operations). UI: summary cards, bar chart, type breakdown, detail tables. 🔍 compare button on observers header.
|
- **#129 OBSERVER COMPARISON PAGE:** New `#/compare` route with shareable params `?a=ID1&b=ID2`. `comparePacketSets()` pure function (O(n) Set operations). UI: summary cards, bar chart, type breakdown, detail tables. 🔍 compare button on observers header.
|
||||||
- **#133 LIVE PAGE NODE PRUNING:** Prune every 60s using `getNodeStatus()` from roles.js (per-role health thresholds: 24h companions/sensors, 72h infrastructure). `_liveSeen` timestamp set on insert, updated on re-observation. Bounded memory usage.
|
- **#133 LIVE PAGE NODE PRUNING:** Prune every 60s using `getNodeStatus()` from roles.js (per-role health thresholds: 24h companions/sensors, 72h infrastructure). `_liveSeen` timestamp set on insert, updated on re-observation. Bounded memory usage.
|
||||||
- **Database merge:** All frontend endpoints working with merged 1.237M observation DB. Load speed verified. All 4 fixes tested end-to-end in browser.
|
- **Database merge:** All frontend endpoints working with merged 1.237M observation DB. Load speed verified. All 4 fixes tested end-to-end in browser.
|
||||||
|
|||||||
@@ -1,50 +1,50 @@
|
|||||||
# Ripley — Support Engineer
|
# Ripley — Support Engineer
|
||||||
|
|
||||||
Deep knowledge of every frontend behavior, API response, and user-facing feature in CoreScope. Fields community questions, triages bug reports, and explains "why does X look like Y."
|
Deep knowledge of every frontend behavior, API response, and user-facing feature in CoreScope. Fields community questions, triages bug reports, and explains "why does X look like Y."
|
||||||
|
|
||||||
## Project Context
|
## Project Context
|
||||||
|
|
||||||
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
|
**Project:** CoreScope — Real-time LoRa mesh packet analyzer
|
||||||
**Stack:** Vanilla JS frontend (public/*.js), Node.js backend, SQLite, WebSocket, MQTT
|
**Stack:** Vanilla JS frontend (public/*.js), Node.js backend, SQLite, WebSocket, MQTT
|
||||||
**User:** Kpa-clawbot
|
**User:** Kpa-clawbot
|
||||||
|
|
||||||
## Responsibilities
|
## Responsibilities
|
||||||
|
|
||||||
- Answer user questions about UI behavior ("why is this node gray?", "why don't I see my repeater?")
|
- Answer user questions about UI behavior ("why is this node gray?", "why don't I see my repeater?")
|
||||||
- Triage community bug reports and feature requests on GitHub issues
|
- Triage community bug reports and feature requests on GitHub issues
|
||||||
- Know every frontend module intimately — read all public/*.js files before answering
|
- Know every frontend module intimately — read all public/*.js files before answering
|
||||||
- Know the API response shapes — what each endpoint returns and how the frontend uses it
|
- Know the API response shapes — what each endpoint returns and how the frontend uses it
|
||||||
- Know the status/health system — roles.js thresholds, active/stale/degraded/silent states
|
- Know the status/health system — roles.js thresholds, active/stale/degraded/silent states
|
||||||
- Know the map behavior — marker colors, opacity, filtering, live vs static
|
- Know the map behavior — marker colors, opacity, filtering, live vs static
|
||||||
- Know the packet display — filter syntax, detail pane, hex breakdown, decoded fields
|
- Know the packet display — filter syntax, detail pane, hex breakdown, decoded fields
|
||||||
- Reproduce reported issues by checking live data via API
|
- Reproduce reported issues by checking live data via API
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
- Does NOT write code — routes fixes to Hicks (backend) or Newt (frontend)
|
- Does NOT write code — routes fixes to Hicks (backend) or Newt (frontend)
|
||||||
- Does NOT deploy — routes to Hudson
|
- Does NOT deploy — routes to Hudson
|
||||||
- MAY comment on GitHub issues with explanations and triage notes
|
- MAY comment on GitHub issues with explanations and triage notes
|
||||||
- MAY suggest workarounds to users while fixes are in progress
|
- MAY suggest workarounds to users while fixes are in progress
|
||||||
|
|
||||||
## Key Knowledge Areas
|
## Key Knowledge Areas
|
||||||
|
|
||||||
- **Node colors/status:** roles.js defines ROLE_COLORS, health thresholds per role. Gray = stale/silent. Dimmed = opacity 0.25 on live map.
|
- **Node colors/status:** roles.js defines ROLE_COLORS, health thresholds per role. Gray = stale/silent. Dimmed = opacity 0.25 on live map.
|
||||||
- **last_heard vs last_seen:** Always prefer `last_heard || last_seen`. last_heard from packet store (all traffic), last_seen from DB (adverts only).
|
- **last_heard vs last_seen:** Always prefer `last_heard || last_seen`. last_heard from packet store (all traffic), last_seen from DB (adverts only).
|
||||||
- **Hash prefixes:** 1-byte or 2-byte hash_size affects node disambiguation. hash_size_inconsistent flag.
|
- **Hash prefixes:** 1-byte or 2-byte hash_size affects node disambiguation. hash_size_inconsistent flag.
|
||||||
- **Packet types:** ADVERT, TXT_MSG, GRP_TXT, REQ, CHAN, POS — what each means.
|
- **Packet types:** ADVERT, TXT_MSG, GRP_TXT, REQ, CHAN, POS — what each means.
|
||||||
- **Observer vs Node:** Observers are MQTT-connected gateways. Nodes are mesh devices.
|
- **Observer vs Node:** Observers are MQTT-connected gateways. Nodes are mesh devices.
|
||||||
- **Live vs Static map:** Live map shows real-time WS data + API nodes. Static map shows all known nodes from API.
|
- **Live vs Static map:** Live map shows real-time WS data + API nodes. Static map shows all known nodes from API.
|
||||||
- **Channel decryption:** channelHashHex, decryptionStatus (decrypted/no_key/decryption_failed)
|
- **Channel decryption:** channelHashHex, decryptionStatus (decrypted/no_key/decryption_failed)
|
||||||
- **Geo filter:** polygon + bufferKm in config.json, excludes nodes outside boundary
|
- **Geo filter:** polygon + bufferKm in config.json, excludes nodes outside boundary
|
||||||
|
|
||||||
## How to Answer Questions
|
## How to Answer Questions
|
||||||
|
|
||||||
1. Read the relevant frontend code FIRST — don't guess
|
1. Read the relevant frontend code FIRST — don't guess
|
||||||
2. Check the live API data if applicable (analyzer.00id.net is public)
|
2. Check the live API data if applicable (analyzer.00id.net is public)
|
||||||
3. Explain in user-friendly terms, not code jargon
|
3. Explain in user-friendly terms, not code jargon
|
||||||
4. If it's a bug, route to the right squad member
|
4. If it's a bug, route to the right squad member
|
||||||
5. If it's expected behavior, explain WHY
|
5. If it's expected behavior, explain WHY
|
||||||
|
|
||||||
## Model
|
## Model
|
||||||
|
|
||||||
Preferred: auto
|
Preferred: auto
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"assignments": [
|
"assignments": [
|
||||||
{
|
{
|
||||||
"assignment_id": "meshcore-analyzer-001",
|
"assignment_id": "meshcore-analyzer-001",
|
||||||
"universe": "aliens",
|
"universe": "aliens",
|
||||||
"created_at": "2026-03-26T04:22:08Z",
|
"created_at": "2026-03-26T04:22:08Z",
|
||||||
"agents": ["Kobayashi", "Hicks", "Newt", "Bishop"],
|
"agents": ["Kobayashi", "Hicks", "Newt", "Bishop"],
|
||||||
"reason": "Initial team casting for CoreScope project"
|
"reason": "Initial team casting for CoreScope project"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"universes_allowed": ["aliens"],
|
"universes_allowed": ["aliens"],
|
||||||
"max_per_universe": 10,
|
"max_per_universe": 10,
|
||||||
"overflow_strategy": "diegetic_expansion"
|
"overflow_strategy": "diegetic_expansion"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,52 @@
|
|||||||
{
|
{
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"persistent_name": "Kobayashi",
|
"persistent_name": "Kobayashi",
|
||||||
"role": "Lead",
|
"role": "Lead",
|
||||||
"universe": "aliens",
|
"universe": "aliens",
|
||||||
"created_at": "2026-03-26T04:22:08Z",
|
"created_at": "2026-03-26T04:22:08Z",
|
||||||
"legacy_named": false,
|
"legacy_named": false,
|
||||||
"status": "active"
|
"status": "active"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"persistent_name": "Hicks",
|
"persistent_name": "Hicks",
|
||||||
"role": "Backend Dev",
|
"role": "Backend Dev",
|
||||||
"universe": "aliens",
|
"universe": "aliens",
|
||||||
"created_at": "2026-03-26T04:22:08Z",
|
"created_at": "2026-03-26T04:22:08Z",
|
||||||
"legacy_named": false,
|
"legacy_named": false,
|
||||||
"status": "active"
|
"status": "active"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"persistent_name": "Newt",
|
"persistent_name": "Newt",
|
||||||
"role": "Frontend Dev",
|
"role": "Frontend Dev",
|
||||||
"universe": "aliens",
|
"universe": "aliens",
|
||||||
"created_at": "2026-03-26T04:22:08Z",
|
"created_at": "2026-03-26T04:22:08Z",
|
||||||
"legacy_named": false,
|
"legacy_named": false,
|
||||||
"status": "active"
|
"status": "active"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"persistent_name": "Bishop",
|
"persistent_name": "Bishop",
|
||||||
"role": "Tester",
|
"role": "Tester",
|
||||||
"universe": "aliens",
|
"universe": "aliens",
|
||||||
"created_at": "2026-03-26T04:22:08Z",
|
"created_at": "2026-03-26T04:22:08Z",
|
||||||
"legacy_named": false,
|
"legacy_named": false,
|
||||||
"status": "active"
|
"status": "active"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"persistent_name": "Hudson",
|
"persistent_name": "Hudson",
|
||||||
"role": "DevOps Engineer",
|
"role": "DevOps Engineer",
|
||||||
"universe": "aliens",
|
"universe": "aliens",
|
||||||
"created_at": "2026-03-27T02:00:00Z",
|
"created_at": "2026-03-27T02:00:00Z",
|
||||||
"legacy_named": false,
|
"legacy_named": false,
|
||||||
"status": "active"
|
"status": "active"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"persistent_name": "Ripley",
|
"persistent_name": "Ripley",
|
||||||
"role": "Support Engineer",
|
"role": "Support Engineer",
|
||||||
"universe": "aliens",
|
"universe": "aliens",
|
||||||
"created_at": "2026-03-27T16:12:00Z",
|
"created_at": "2026-03-27T16:12:00Z",
|
||||||
"legacy_named": false,
|
"legacy_named": false,
|
||||||
"status": "active"
|
"status": "active"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
# Ceremonies
|
# Ceremonies
|
||||||
|
|
||||||
> Team meetings that happen before or after work. Each squad configures their own.
|
> Team meetings that happen before or after work. Each squad configures their own.
|
||||||
|
|
||||||
## Design Review
|
## Design Review
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|-------|-------|
|
|-------|-------|
|
||||||
| **Trigger** | auto |
|
| **Trigger** | auto |
|
||||||
| **When** | before |
|
| **When** | before |
|
||||||
| **Condition** | multi-agent task involving 2+ agents modifying shared systems |
|
| **Condition** | multi-agent task involving 2+ agents modifying shared systems |
|
||||||
| **Facilitator** | lead |
|
| **Facilitator** | lead |
|
||||||
| **Participants** | all-relevant |
|
| **Participants** | all-relevant |
|
||||||
| **Time budget** | focused |
|
| **Time budget** | focused |
|
||||||
| **Enabled** | ✅ yes |
|
| **Enabled** | ✅ yes |
|
||||||
|
|
||||||
**Agenda:**
|
**Agenda:**
|
||||||
1. Review the task and requirements
|
1. Review the task and requirements
|
||||||
2. Agree on interfaces and contracts between components
|
2. Agree on interfaces and contracts between components
|
||||||
3. Identify risks and edge cases
|
3. Identify risks and edge cases
|
||||||
4. Assign action items
|
4. Assign action items
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Retrospective
|
## Retrospective
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|-------|-------|
|
|-------|-------|
|
||||||
| **Trigger** | auto |
|
| **Trigger** | auto |
|
||||||
| **When** | after |
|
| **When** | after |
|
||||||
| **Condition** | build failure, test failure, or reviewer rejection |
|
| **Condition** | build failure, test failure, or reviewer rejection |
|
||||||
| **Facilitator** | lead |
|
| **Facilitator** | lead |
|
||||||
| **Participants** | all-involved |
|
| **Participants** | all-involved |
|
||||||
| **Time budget** | focused |
|
| **Time budget** | focused |
|
||||||
| **Enabled** | ✅ yes |
|
| **Enabled** | ✅ yes |
|
||||||
|
|
||||||
**Agenda:**
|
**Agenda:**
|
||||||
1. What happened? (facts only)
|
1. What happened? (facts only)
|
||||||
2. Root cause analysis
|
2. Root cause analysis
|
||||||
3. What should change?
|
3. What should change?
|
||||||
4. Action items for next iteration
|
4. Action items for next iteration
|
||||||
|
|||||||
@@ -1,354 +1,354 @@
|
|||||||
# Squad Decisions Log
|
# Squad Decisions Log
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Decision: User Directives
|
## Decision: User Directives
|
||||||
|
|
||||||
### 2026-03-27T04:27 — Docker Compose v2 Plugin Check
|
### 2026-03-27T04:27 — Docker Compose v2 Plugin Check
|
||||||
**By:** User (via Copilot)
|
**By:** User (via Copilot)
|
||||||
**Decision:** CI pipeline should check if `docker compose` (v2 plugin) is installed on the self-hosted runner and install it if needed, as part of the deploy job itself.
|
**Decision:** CI pipeline should check if `docker compose` (v2 plugin) is installed on the self-hosted runner and install it if needed, as part of the deploy job itself.
|
||||||
**Rationale:** Self-healing CI is preferred over manual VM setup; the VM may not have docker compose v2 installed.
|
**Rationale:** Self-healing CI is preferred over manual VM setup; the VM may not have docker compose v2 installed.
|
||||||
|
|
||||||
### 2026-03-27T04:39 — Staging DB: Use Old Problematic DB
|
### 2026-03-27T04:39 — Staging DB: Use Old Problematic DB
|
||||||
**By:** User (via Copilot)
|
**By:** User (via Copilot)
|
||||||
**Decision:** Staging environment's primary purpose is debugging the problematic DB that caused 100% CPU on prod. Use the old DB (`~/meshcore-data-old/` on the VM) for staging. Prod keeps its current (new) DB. Never put the problematic DB on prod.
|
**Decision:** Staging environment's primary purpose is debugging the problematic DB that caused 100% CPU on prod. Use the old DB (`~/meshcore-data-old/` on the VM) for staging. Prod keeps its current (new) DB. Never put the problematic DB on prod.
|
||||||
**Rationale:** This is the reason the staging environment was built.
|
**Rationale:** This is the reason the staging environment was built.
|
||||||
|
|
||||||
### 2026-03-27T06:09 — Plan Go Rewrite (MQTT Separation)
|
### 2026-03-27T06:09 — Plan Go Rewrite (MQTT Separation)
|
||||||
**By:** User (via Copilot)
|
**By:** User (via Copilot)
|
||||||
**Decision:** Start planning a Go rewrite. First step: separate MQTT ingestion (writes to DB) from the web server (reads from DB + serves API/frontend). Two separate services.
|
**Decision:** Start planning a Go rewrite. First step: separate MQTT ingestion (writes to DB) from the web server (reads from DB + serves API/frontend). Two separate services.
|
||||||
**Rationale:** Node.js single-thread + V8 heap limitations cause fragility at scale (185MB DB → 2.7GB heap → OOM). Go eliminates heap cap problem and enables real concurrency.
|
**Rationale:** Node.js single-thread + V8 heap limitations cause fragility at scale (185MB DB → 2.7GB heap → OOM). Go eliminates heap cap problem and enables real concurrency.
|
||||||
|
|
||||||
### 2026-03-27T06:31 — NO PII in Git
|
### 2026-03-27T06:31 — NO PII in Git
|
||||||
**By:** User (via Copilot)
|
**By:** User (via Copilot)
|
||||||
**Decision:** NEVER write real names, usernames, email addresses, or any PII to files committed to git. Use "User" for attribution and "deploy" for SSH/server references. This is a PUBLIC repo.
|
**Decision:** NEVER write real names, usernames, email addresses, or any PII to files committed to git. Use "User" for attribution and "deploy" for SSH/server references. This is a PUBLIC repo.
|
||||||
**Rationale:** PII was leaked to the public repo and required a full git history rewrite to remove.
|
**Rationale:** PII was leaked to the public repo and required a full git history rewrite to remove.
|
||||||
|
|
||||||
### 2026-03-27T02:19 — Production/Infrastructure Touches: Hudson Only
|
### 2026-03-27T02:19 — Production/Infrastructure Touches: Hudson Only
|
||||||
**By:** User (via Copilot)
|
**By:** User (via Copilot)
|
||||||
**Decision:** Production/infrastructure touches (SSH, DB ops, server restarts, Azure operations) should only be done by Hudson (DevOps). No other agents should touch prod directly.
|
**Decision:** Production/infrastructure touches (SSH, DB ops, server restarts, Azure operations) should only be done by Hudson (DevOps). No other agents should touch prod directly.
|
||||||
**Rationale:** Separation of concerns — dev agents write code, DevOps deploys and manages prod.
|
**Rationale:** Separation of concerns — dev agents write code, DevOps deploys and manages prod.
|
||||||
|
|
||||||
### 2026-03-27T03:36 — Staging Environment Architecture
|
### 2026-03-27T03:36 — Staging Environment Architecture
|
||||||
**By:** User (via Copilot)
|
**By:** User (via Copilot)
|
||||||
**Decision:**
|
**Decision:**
|
||||||
1. No Docker named volumes — always bind mount from `~/meshcore-data` (host location, easy to access)
|
1. No Docker named volumes — always bind mount from `~/meshcore-data` (host location, easy to access)
|
||||||
2. Staging container runs on plaintext port (e.g., port 81, no HTTPS)
|
2. Staging container runs on plaintext port (e.g., port 81, no HTTPS)
|
||||||
3. Use Docker Compose to orchestrate prod + staging containers on the same VM
|
3. Use Docker Compose to orchestrate prod + staging containers on the same VM
|
||||||
4. `manage.sh` supports launching prod only OR prod+staging with clear messaging
|
4. `manage.sh` supports launching prod only OR prod+staging with clear messaging
|
||||||
5. Ports must be configurable via `manage.sh` or environment, with sane defaults
|
5. Ports must be configurable via `manage.sh` or environment, with sane defaults
|
||||||
|
|
||||||
### 2026-03-27T03:43 — Staging Refinements: Shared Data
|
### 2026-03-27T03:43 — Staging Refinements: Shared Data
|
||||||
**By:** User (via Copilot)
|
**By:** User (via Copilot)
|
||||||
**Decision:**
|
**Decision:**
|
||||||
1. Staging copies prod DB on launch (snapshot into staging data dir when started)
|
1. Staging copies prod DB on launch (snapshot into staging data dir when started)
|
||||||
2. Staging connects to SAME MQTT broker as prod (not its own Mosquitto)
|
2. Staging connects to SAME MQTT broker as prod (not its own Mosquitto)
|
||||||
|
|
||||||
**Rationale:** Staging needs real data (prod-like conditions) to be useful for testing.
|
**Rationale:** Staging needs real data (prod-like conditions) to be useful for testing.
|
||||||
|
|
||||||
### 2026-03-27T17:13 — Scribe Auto-Run After Agent Batches
|
### 2026-03-27T17:13 — Scribe Auto-Run After Agent Batches
|
||||||
**By:** User (via Copilot)
|
**By:** User (via Copilot)
|
||||||
**Decision:** Scribe must run after EVERY batch of agent work automatically. No manual triggers. No reminders needed. This is a process guarantee, not a suggestion.
|
**Decision:** Scribe must run after EVERY batch of agent work automatically. No manual triggers. No reminders needed. This is a process guarantee, not a suggestion.
|
||||||
**Rationale:** Coordinator has been forgetting to spawn Scribe after agent batches complete. This is a process failure. Scribe auto-spawn ends the forgetfulness.
|
**Rationale:** Coordinator has been forgetting to spawn Scribe after agent batches complete. This is a process failure. Scribe auto-spawn ends the forgetfulness.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Decision: Technical Fixes
|
## Decision: Technical Fixes
|
||||||
|
|
||||||
### Issue #126 — Skip Ambiguous Hop Prefixes
|
### Issue #126 — Skip Ambiguous Hop Prefixes
|
||||||
**By:** Hicks (Backend Dev)
|
**By:** Hicks (Backend Dev)
|
||||||
**Date:** 2026-03-27
|
**Date:** 2026-03-27
|
||||||
**Status:** Implemented
|
**Status:** Implemented
|
||||||
|
|
||||||
When resolving hop prefixes to full node pubkeys, require a **unique match**. If prefix matches 2+ nodes in DB, skip it and cache in `ambiguousHopPrefixes` (negative cache). Prevents hash prefix collisions (e.g., `1CC4` vs `1C82` sharing prefix `1C` under 1-byte hash_size) from attributing packets to wrong nodes.
|
When resolving hop prefixes to full node pubkeys, require a **unique match**. If prefix matches 2+ nodes in DB, skip it and cache in `ambiguousHopPrefixes` (negative cache). Prevents hash prefix collisions (e.g., `1CC4` vs `1C82` sharing prefix `1C` under 1-byte hash_size) from attributing packets to wrong nodes.
|
||||||
|
|
||||||
**Impact:**
|
**Impact:**
|
||||||
- Hopresixes that collide won't update `lastPathSeenMap` for any node (conservative, correct)
|
- Hopresixes that collide won't update `lastPathSeenMap` for any node (conservative, correct)
|
||||||
- `disambiguateHops()` still does geometric disambiguation for route visualization
|
- `disambiguateHops()` still does geometric disambiguation for route visualization
|
||||||
- Performance: `LIMIT 2` query efficient; ambiguous results cached
|
- Performance: `LIMIT 2` query efficient; ambiguous results cached
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Issue #133 — Phantom Nodes & Active Window
|
### Issue #133 — Phantom Nodes & Active Window
|
||||||
**By:** Hicks (Backend Dev)
|
**By:** Hicks (Backend Dev)
|
||||||
**Date:** 2026-03-27
|
**Date:** 2026-03-27
|
||||||
**Status:** Implemented
|
**Status:** Implemented
|
||||||
|
|
||||||
**Part 1: Remove phantom node creation**
|
**Part 1: Remove phantom node creation**
|
||||||
- `autoLearnHopNodes()` no longer calls `db.upsertNode()` for unresolved hops
|
- `autoLearnHopNodes()` no longer calls `db.upsertNode()` for unresolved hops
|
||||||
- Added `db.removePhantomNodes()` — deletes nodes where `LENGTH(public_key) <= 16` (real keys are 64 hex chars)
|
- Added `db.removePhantomNodes()` — deletes nodes where `LENGTH(public_key) <= 16` (real keys are 64 hex chars)
|
||||||
- Called at startup to purge existing phantoms from prior behavior
|
- Called at startup to purge existing phantoms from prior behavior
|
||||||
- Hop-resolver still handles unresolved prefixes gracefully
|
- Hop-resolver still handles unresolved prefixes gracefully
|
||||||
|
|
||||||
**Part 2: totalNodes now 7-day active window**
|
**Part 2: totalNodes now 7-day active window**
|
||||||
- `/api/stats` `totalNodes` returns only nodes seen in last 7 days (was all-time)
|
- `/api/stats` `totalNodes` returns only nodes seen in last 7 days (was all-time)
|
||||||
- New field `totalNodesAllTime` for historical tracking
|
- New field `totalNodesAllTime` for historical tracking
|
||||||
- Role counts (repeaters, rooms, companions, sensors) also filtered to 7-day window
|
- Role counts (repeaters, rooms, companions, sensors) also filtered to 7-day window
|
||||||
- Frontend: no changes needed (same field name, smaller correct number)
|
- Frontend: no changes needed (same field name, smaller correct number)
|
||||||
|
|
||||||
**Impact:** Frontend `totalNodes` now reflects active mesh size. Go server should apply same 7-day filter when querying.
|
**Impact:** Frontend `totalNodes` now reflects active mesh size. Go server should apply same 7-day filter when querying.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Issue #123 — Channel Hash on Undecrypted Messages
|
### Issue #123 — Channel Hash on Undecrypted Messages
|
||||||
**By:** Hicks
|
**By:** Hicks
|
||||||
**Status:** Implemented
|
**Status:** Implemented
|
||||||
|
|
||||||
Fixed test coverage for decrypted status tracking on channel messages.
|
Fixed test coverage for decrypted status tracking on channel messages.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Issue #130 — Live Map: Dim Stale Nodes, Don't Remove
|
### Issue #130 — Live Map: Dim Stale Nodes, Don't Remove
|
||||||
**By:** Newt (Frontend)
|
**By:** Newt (Frontend)
|
||||||
**Date:** 2026-03-27
|
**Date:** 2026-03-27
|
||||||
**Status:** Implemented
|
**Status:** Implemented
|
||||||
|
|
||||||
`pruneStaleNodes()` in `live.js` now distinguishes API-loaded nodes (`_fromAPI`) from WS-only dynamic nodes. API nodes dimmed (reduced opacity) when stale instead of removed. WS-only nodes still pruned to prevent memory leaks.
|
`pruneStaleNodes()` in `live.js` now distinguishes API-loaded nodes (`_fromAPI`) from WS-only dynamic nodes. API nodes dimmed (reduced opacity) when stale instead of removed. WS-only nodes still pruned to prevent memory leaks.
|
||||||
|
|
||||||
**Rationale:** Static map shows stale nodes with faded markers; live map was deleting them, causing user-reported disappearing nodes. Parity expected.
|
**Rationale:** Static map shows stale nodes with faded markers; live map was deleting them, causing user-reported disappearing nodes. Parity expected.
|
||||||
|
|
||||||
**Pattern:** Database-loaded nodes never removed from map during session. Future live map features should respect `_fromAPI` flag.
|
**Pattern:** Database-loaded nodes never removed from map during session. Future live map features should respect `_fromAPI` flag.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Issue #131 — Nodes Tab Auto-Update via WebSocket
|
### Issue #131 — Nodes Tab Auto-Update via WebSocket
|
||||||
**By:** Newt (Frontend)
|
**By:** Newt (Frontend)
|
||||||
**Date:** 2026-03-27
|
**Date:** 2026-03-27
|
||||||
**Status:** Implemented
|
**Status:** Implemented
|
||||||
|
|
||||||
WS-driven page updates must reset local caches: (1) set local cache to null, (2) call `invalidateApiCache()`, (3) re-fetch. New `loadNodes(refreshOnly)` pattern skips full DOM rebuild, only updates data rows. Preserves scroll, selection, listeners.
|
WS-driven page updates must reset local caches: (1) set local cache to null, (2) call `invalidateApiCache()`, (3) re-fetch. New `loadNodes(refreshOnly)` pattern skips full DOM rebuild, only updates data rows. Preserves scroll, selection, listeners.
|
||||||
|
|
||||||
**Trap:** Two-layer caching (local variable + API cache) prevents re-fetches. All three reset steps required.
|
**Trap:** Two-layer caching (local variable + API cache) prevents re-fetches. All three reset steps required.
|
||||||
|
|
||||||
**Pattern:** Other pages doing WS-driven updates should follow same approach.
|
**Pattern:** Other pages doing WS-driven updates should follow same approach.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Issue #129 — Observer Comparison Page
|
### Issue #129 — Observer Comparison Page
|
||||||
**By:** Newt (Frontend)
|
**By:** Newt (Frontend)
|
||||||
**Date:** 2026-03-27
|
**Date:** 2026-03-27
|
||||||
**Status:** Implemented
|
**Status:** Implemented
|
||||||
|
|
||||||
Added `comparePacketSets(hashesA, hashesB)` as standalone pure function exposed on `window` for testability. Computes `{ onlyA, onlyB, both }` via Set operations (O(n)).
|
Added `comparePacketSets(hashesA, hashesB)` as standalone pure function exposed on `window` for testability. Computes `{ onlyA, onlyB, both }` via Set operations (O(n)).
|
||||||
|
|
||||||
**Pattern:** Comparison logic decoupled from UI, reusable. Client-side diff avoids new server endpoint. 24-hour window keeps data size reasonable (~10K packets max).
|
**Pattern:** Comparison logic decoupled from UI, reusable. Client-side diff avoids new server endpoint. 24-hour window keeps data size reasonable (~10K packets max).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Issue #132 — Detail Pane Collapse
|
### Issue #132 — Detail Pane Collapse
|
||||||
**By:** Newt (Frontend)
|
**By:** Newt (Frontend)
|
||||||
**Date:** 2026-03-27
|
**Date:** 2026-03-27
|
||||||
**Status:** Implemented
|
**Status:** Implemented
|
||||||
|
|
||||||
Detail pane collapse uses CSS class on parent container. Add `detail-collapsed` class to `.split-layout`, which sets `.panel-right` to `display: none`. `.panel-left` with `flex: 1` fills 100% width naturally.
|
Detail pane collapse uses CSS class on parent container. Add `detail-collapsed` class to `.split-layout`, which sets `.panel-right` to `display: none`. `.panel-left` with `flex: 1` fills 100% width naturally.
|
||||||
|
|
||||||
**Pattern:** CSS class toggling on parent cleaner than inline styles, easier to animate, keeps layout logic in CSS.
|
**Pattern:** CSS class toggling on parent cleaner than inline styles, easier to animate, keeps layout logic in CSS.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Decision: Infrastructure & Deployment
|
## Decision: Infrastructure & Deployment
|
||||||
|
|
||||||
### Database Merge — Prod + Staging
|
### Database Merge — Prod + Staging
|
||||||
**By:** Kobayashi (Lead) / Hudson (DevOps)
|
**By:** Kobayashi (Lead) / Hudson (DevOps)
|
||||||
**Date:** 2026-03-27
|
**Date:** 2026-03-27
|
||||||
**Status:** ✅ Complete
|
**Status:** ✅ Complete
|
||||||
|
|
||||||
Merged staging DB (185MB, 50K transmissions + 1.2M observations) into prod DB (21MB). Dedup strategy:
|
Merged staging DB (185MB, 50K transmissions + 1.2M observations) into prod DB (21MB). Dedup strategy:
|
||||||
- **Transmissions:** `INSERT OR IGNORE` on `hash` (unique key)
|
- **Transmissions:** `INSERT OR IGNORE` on `hash` (unique key)
|
||||||
- **Observations:** All unique by observer, all preserved
|
- **Observations:** All unique by observer, all preserved
|
||||||
- **Nodes/Observers:** Latest `last_seen` wins, sum counts
|
- **Nodes/Observers:** Latest `last_seen` wins, sum counts
|
||||||
|
|
||||||
**Results:**
|
**Results:**
|
||||||
- Merged DB: 51,723 transmissions, 1,237,186 observations
|
- Merged DB: 51,723 transmissions, 1,237,186 observations
|
||||||
- Deployment: Docker Compose managed `meshcore-prod` with bind mounts
|
- Deployment: Docker Compose managed `meshcore-prod` with bind mounts
|
||||||
- Load time: 8,491ms, Memory: 860MiB RSS (no NODE_OPTIONS needed, RAM fix effective)
|
- Load time: 8,491ms, Memory: 860MiB RSS (no NODE_OPTIONS needed, RAM fix effective)
|
||||||
- Downtime: ~2 minutes
|
- Downtime: ~2 minutes
|
||||||
- Backups: Retained at `/home/deploy/backups/pre-merge-20260327-071425/` until 2026-04-03
|
- Backups: Retained at `/home/deploy/backups/pre-merge-20260327-071425/` until 2026-04-03
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Unified Docker Volume Paths
|
### Unified Docker Volume Paths
|
||||||
**By:** Hudson (DevOps)
|
**By:** Hudson (DevOps)
|
||||||
**Date:** 2026-03-27
|
**Date:** 2026-03-27
|
||||||
**Status:** Applied
|
**Status:** Applied
|
||||||
|
|
||||||
Reconciled `manage.sh` and `docker-compose.yml` Docker volume names:
|
Reconciled `manage.sh` and `docker-compose.yml` Docker volume names:
|
||||||
- Caddy volume: `caddy-data` everywhere (prod); `caddy-data-staging` for staging
|
- Caddy volume: `caddy-data` everywhere (prod); `caddy-data-staging` for staging
|
||||||
- Data directory: Bind mount via `PROD_DATA_DIR` env var, default `~/meshcore-data`
|
- Data directory: Bind mount via `PROD_DATA_DIR` env var, default `~/meshcore-data`
|
||||||
- Config/Caddyfile: Mounted from repo checkout for prod, staging data dir for staging
|
- Config/Caddyfile: Mounted from repo checkout for prod, staging data dir for staging
|
||||||
- Removed deprecated `version` key from docker-compose.yml
|
- Removed deprecated `version` key from docker-compose.yml
|
||||||
|
|
||||||
**Consequence:** `./manage.sh start` and `docker compose up prod` now produce identical mounts. Anyone with data in old `caddy-data-prod` volume will need Caddy to re-provision TLS certs automatically.
|
**Consequence:** `./manage.sh start` and `docker compose up prod` now produce identical mounts. Anyone with data in old `caddy-data-prod` volume will need Caddy to re-provision TLS certs automatically.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Staging DB Setup & Production Data Locations
|
### Staging DB Setup & Production Data Locations
|
||||||
**By:** Hudson (DevOps)
|
**By:** Hudson (DevOps)
|
||||||
**Date:** 2026-03-27
|
**Date:** 2026-03-27
|
||||||
**Status:** Implemented
|
**Status:** Implemented
|
||||||
|
|
||||||
**Production Data Locations:**
|
**Production Data Locations:**
|
||||||
- **Prod DB:** Docker volume `meshcore-data` → `/var/lib/docker/volumes/meshcore-data/_data/meshcore.db` (21MB, fresh)
|
- **Prod DB:** Docker volume `meshcore-data` → `/var/lib/docker/volumes/meshcore-data/_data/meshcore.db` (21MB, fresh)
|
||||||
- **Prod config:** `/home/deploy/meshcore-analyzer/config.json` (bind mount, read-only)
|
- **Prod config:** `/home/deploy/meshcore-analyzer/config.json` (bind mount, read-only)
|
||||||
- **Caddyfile:** `/home/deploy/meshcore-analyzer/caddy-config/Caddyfile` (bind mount, read-only)
|
- **Caddyfile:** `/home/deploy/meshcore-analyzer/caddy-config/Caddyfile` (bind mount, read-only)
|
||||||
- **Old (broken) DB:** `~/meshcore-data-old/meshcore.db` (185MB, DO NOT DELETE)
|
- **Old (broken) DB:** `~/meshcore-data-old/meshcore.db` (185MB, DO NOT DELETE)
|
||||||
- **Staging data:** `~/meshcore-staging-data/` (copy of broken DB + config)
|
- **Staging data:** `~/meshcore-staging-data/` (copy of broken DB + config)
|
||||||
|
|
||||||
**Rules:**
|
**Rules:**
|
||||||
- DO NOT delete `~/meshcore-data-old/` — backup of problematic DB
|
- DO NOT delete `~/meshcore-data-old/` — backup of problematic DB
|
||||||
- DO NOT modify staging DB before staging container ready
|
- DO NOT modify staging DB before staging container ready
|
||||||
- Only Hudson touches prod infrastructure
|
- Only Hudson touches prod infrastructure
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Decision: Go Rewrite — API & Storage
|
## Decision: Go Rewrite — API & Storage
|
||||||
|
|
||||||
### Go MQTT Ingestor (cmd/ingestor/)
|
### Go MQTT Ingestor (cmd/ingestor/)
|
||||||
**By:** Hicks (Backend Dev)
|
**By:** Hicks (Backend Dev)
|
||||||
**Date:** 2026-03-27
|
**Date:** 2026-03-27
|
||||||
**Status:** Implemented, 25 tests passing
|
**Status:** Implemented, 25 tests passing
|
||||||
|
|
||||||
Standalone Go MQTT ingestor service. Separate process from Node.js web server that handles MQTT packet ingestion + writes to shared SQLite DB.
|
Standalone Go MQTT ingestor service. Separate process from Node.js web server that handles MQTT packet ingestion + writes to shared SQLite DB.
|
||||||
|
|
||||||
**Architecture:**
|
**Architecture:**
|
||||||
- Single binary, no CGO (uses `modernc.org/sqlite` pure Go)
|
- Single binary, no CGO (uses `modernc.org/sqlite` pure Go)
|
||||||
- Reads same `config.json` (mqttSources array)
|
- Reads same `config.json` (mqttSources array)
|
||||||
- Shares SQLite DB with Node.js (WAL mode for concurrent access)
|
- Shares SQLite DB with Node.js (WAL mode for concurrent access)
|
||||||
- Format 1 (raw packet) MQTT only — companion bridge stays in Node.js
|
- Format 1 (raw packet) MQTT only — companion bridge stays in Node.js
|
||||||
- No HTTP/WebSocket — web layer stays in Node.js
|
- No HTTP/WebSocket — web layer stays in Node.js
|
||||||
|
|
||||||
**Ported from decoder.js:**
|
**Ported from decoder.js:**
|
||||||
- Packet header/path/payloads, advert with flags/lat/lon/name
|
- Packet header/path/payloads, advert with flags/lat/lon/name
|
||||||
- computeContentHash (SHA-256, path-independent)
|
- computeContentHash (SHA-256, path-independent)
|
||||||
- db.js v3 schema (transmissions, observations, nodes, observers)
|
- db.js v3 schema (transmissions, observations, nodes, observers)
|
||||||
- MQTT connection logic (multi-broker, reconnect, IATA filter)
|
- MQTT connection logic (multi-broker, reconnect, IATA filter)
|
||||||
|
|
||||||
**Not Ported:** Companion bridge format, channel key decryption, WebSocket broadcast, in-memory packet store.
|
**Not Ported:** Companion bridge format, channel key decryption, WebSocket broadcast, in-memory packet store.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Go Web Server (cmd/server/)
|
### Go Web Server (cmd/server/)
|
||||||
**By:** Hicks (Backend Dev)
|
**By:** Hicks (Backend Dev)
|
||||||
**Date:** 2026-03-27
|
**Date:** 2026-03-27
|
||||||
**Status:** Implemented, 42 tests passing, `go vet` clean
|
**Status:** Implemented, 42 tests passing, `go vet` clean
|
||||||
|
|
||||||
Standalone Go web server replacing Node.js server's READ side (REST API + WebSocket). Two-component rewrite: ingestor (MQTT writes), server (REST/WS reads).
|
Standalone Go web server replacing Node.js server's READ side (REST API + WebSocket). Two-component rewrite: ingestor (MQTT writes), server (REST/WS reads).
|
||||||
|
|
||||||
**Architecture Decisions:**
|
**Architecture Decisions:**
|
||||||
1. **Direct SQLite queries** — No in-memory packet store; all reads via `packets_v` view (v3 schema)
|
1. **Direct SQLite queries** — No in-memory packet store; all reads via `packets_v` view (v3 schema)
|
||||||
2. **Per-module go.mod** — Each `cmd/*` directory has own `go.mod`
|
2. **Per-module go.mod** — Each `cmd/*` directory has own `go.mod`
|
||||||
3. **gorilla/mux for routing** — Handles 35+ parameterized routes cleanly
|
3. **gorilla/mux for routing** — Handles 35+ parameterized routes cleanly
|
||||||
4. **SQLite polling for WebSocket** — Polls for new transmission IDs every 1s (decouples from MQTT)
|
4. **SQLite polling for WebSocket** — Polls for new transmission IDs every 1s (decouples from MQTT)
|
||||||
5. **Analytics stubs** — Topology, distance, hash-sizes, subpath return valid structural responses (empty data). RF/channels implemented via SQL.
|
5. **Analytics stubs** — Topology, distance, hash-sizes, subpath return valid structural responses (empty data). RF/channels implemented via SQL.
|
||||||
6. **Response shape compatibility** — All endpoints return JSON matching Node.js exactly (frontend works unchanged)
|
6. **Response shape compatibility** — All endpoints return JSON matching Node.js exactly (frontend works unchanged)
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- `cmd/server/main.go` — Entry, HTTP, graceful shutdown
|
- `cmd/server/main.go` — Entry, HTTP, graceful shutdown
|
||||||
- `cmd/server/db.go` — SQLite read queries
|
- `cmd/server/db.go` — SQLite read queries
|
||||||
- `cmd/server/routes.go` — 35+ REST API handlers
|
- `cmd/server/routes.go` — 35+ REST API handlers
|
||||||
- `cmd/server/websocket.go` — Hub + SQLite poller
|
- `cmd/server/websocket.go` — Hub + SQLite poller
|
||||||
- `cmd/server/README.md` — Build/run docs
|
- `cmd/server/README.md` — Build/run docs
|
||||||
|
|
||||||
**Future Work:** Full analytics via SQL, TTL response cache, shared `internal/db/` package, TLS, region-aware filtering.
|
**Future Work:** Full analytics via SQL, TTL response cache, shared `internal/db/` package, TLS, region-aware filtering.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Go API Parity: Transmission-Centric Queries
|
### Go API Parity: Transmission-Centric Queries
|
||||||
**By:** Hicks (Backend Dev)
|
**By:** Hicks (Backend Dev)
|
||||||
**Date:** 2026-03-27
|
**Date:** 2026-03-27
|
||||||
**Status:** Implemented, all 42+ tests pass
|
**Status:** Implemented, all 42+ tests pass
|
||||||
|
|
||||||
Go server rewrote packet list queries from VIEW-based (slow, wrong shape) to **transmission-centric** with correlated subqueries. Schema version detection (`isV3` flag) handles both v2 and v3 schemas.
|
Go server rewrote packet list queries from VIEW-based (slow, wrong shape) to **transmission-centric** with correlated subqueries. Schema version detection (`isV3` flag) handles both v2 and v3 schemas.
|
||||||
|
|
||||||
**Performance Fix:** `/api/packets?groupByHash=true` — 8s → <100ms (query `transmissions` table 52K rows instead of `packets_v` 1.2M observations).
|
**Performance Fix:** `/api/packets?groupByHash=true` — 8s → <100ms (query `transmissions` table 52K rows instead of `packets_v` 1.2M observations).
|
||||||
|
|
||||||
**Field Parity:**
|
**Field Parity:**
|
||||||
- `totalNodes` now 7-day active window (was all-time)
|
- `totalNodes` now 7-day active window (was all-time)
|
||||||
- Added `totalNodesAllTime` field
|
- Added `totalNodesAllTime` field
|
||||||
- Role counts use 7-day filter (matches Node.js line 880-886)
|
- Role counts use 7-day filter (matches Node.js line 880-886)
|
||||||
- `/api/nodes` counts use no time filter; `/api/stats` uses 7-day (separate methods avoid conflation)
|
- `/api/nodes` counts use no time filter; `/api/stats` uses 7-day (separate methods avoid conflation)
|
||||||
- `/api/packets/:id` now parses `path_json`, returns actual hop array
|
- `/api/packets/:id` now parses `path_json`, returns actual hop array
|
||||||
- `/api/observers` — packetsLastHour, lat, lon, nodeRole computed from SQL
|
- `/api/observers` — packetsLastHour, lat, lon, nodeRole computed from SQL
|
||||||
- `/api/nodes/bulk-health` — Per-node stats computed (was returning zeros)
|
- `/api/nodes/bulk-health` — Per-node stats computed (was returning zeros)
|
||||||
- `/api/packets` — Multi-node filter support (`nodes` query param, comma-separated pubkeys)
|
- `/api/packets` — Multi-node filter support (`nodes` query param, comma-separated pubkeys)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Go In-Memory Packet Store (cmd/server/store.go)
|
### Go In-Memory Packet Store (cmd/server/store.go)
|
||||||
**By:** Hicks (Backend Dev)
|
**By:** Hicks (Backend Dev)
|
||||||
**Date:** 2026-03-26
|
**Date:** 2026-03-26
|
||||||
**Status:** Implemented
|
**Status:** Implemented
|
||||||
|
|
||||||
Port of `packet-store.js` with streaming load, 5 indexes, lean observation structs (only observation-specific fields). `QueryPackets` handles type, route, observer, hash, since, until, region, node. `IngestNewFromDB()` streams new transmissions from DB into memory.
|
Port of `packet-store.js` with streaming load, 5 indexes, lean observation structs (only observation-specific fields). `QueryPackets` handles type, route, observer, hash, since, until, region, node. `IngestNewFromDB()` streams new transmissions from DB into memory.
|
||||||
|
|
||||||
**Trade-offs:**
|
**Trade-offs:**
|
||||||
- Memory: ~450 bytes/tx + ~100 bytes/obs (52K tx + 1.2M obs ≈ ~143MB)
|
- Memory: ~450 bytes/tx + ~100 bytes/obs (52K tx + 1.2M obs ≈ ~143MB)
|
||||||
- Startup: One-time load adds few seconds (acceptable)
|
- Startup: One-time load adds few seconds (acceptable)
|
||||||
- DB still used for: analytics, node/observer queries, role counts, region resolution
|
- DB still used for: analytics, node/observer queries, role counts, region resolution
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Observation RAM Optimization
|
### Observation RAM Optimization
|
||||||
**By:** Hicks (Backend Dev)
|
**By:** Hicks (Backend Dev)
|
||||||
**Date:** 2026-03-27
|
**Date:** 2026-03-27
|
||||||
**Status:** Implemented
|
**Status:** Implemented
|
||||||
|
|
||||||
Observation objects in in-memory packet store now store only `transmission_id` reference instead of copying `hash`, `raw_hex`, `decoded_json`, `payload_type`, `route_type` from parent. API boundary methods (`getById`, `getSiblings`, `enrichObservations`) hydrate on demand. Load uses `.iterate()` instead of `.all()` to avoid materializing full JOIN.
|
Observation objects in in-memory packet store now store only `transmission_id` reference instead of copying `hash`, `raw_hex`, `decoded_json`, `payload_type`, `route_type` from parent. API boundary methods (`getById`, `getSiblings`, `enrichObservations`) hydrate on demand. Load uses `.iterate()` instead of `.all()` to avoid materializing full JOIN.
|
||||||
|
|
||||||
**Impact:** Eliminates ~1.17M redundant string copies, avoids 1.17M-row array during startup. 2.7GB RAM → acceptable levels with 185MB database.
|
**Impact:** Eliminates ~1.17M redundant string copies, avoids 1.17M-row array during startup. 2.7GB RAM → acceptable levels with 185MB database.
|
||||||
|
|
||||||
**Code Pattern:** Any code reading observation objects from `tx.observations` directly must use `pktStore.enrichObservations()` if it needs transmission fields. Internal iteration over observations for observer_id, snr, rssi, path_json works unchanged.
|
**Code Pattern:** Any code reading observation objects from `tx.observations` directly must use `pktStore.enrichObservations()` if it needs transmission fields. Internal iteration over observations for observer_id, snr, rssi, path_json works unchanged.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Decision: E2E Playwright Performance Improvements
|
## Decision: E2E Playwright Performance Improvements
|
||||||
|
|
||||||
**Author:** Kobayashi (Lead)
|
**Author:** Kobayashi (Lead)
|
||||||
**Date:** 2026-03-26
|
**Date:** 2026-03-26
|
||||||
**Status:** Proposed — awaiting user sign-off before implementation
|
**Status:** Proposed — awaiting user sign-off before implementation
|
||||||
|
|
||||||
Playwright E2E tests (16 tests in `test-e2e-playwright.js`) are slow in CI. Analysis identified ~40-50% potential runtime reduction.
|
Playwright E2E tests (16 tests in `test-e2e-playwright.js`) are slow in CI. Analysis identified ~40-50% potential runtime reduction.
|
||||||
|
|
||||||
### Recommendations (prioritized)
|
### Recommendations (prioritized)
|
||||||
|
|
||||||
#### HIGH impact (30%+ improvement)
|
#### HIGH impact (30%+ improvement)
|
||||||
|
|
||||||
1. **Replace `waitUntil: 'networkidle'` with `'domcontentloaded'` + targeted waits** — used ~20 times; `networkidle` worst-case for SPAs with persistent WebSocket + Leaflet tile loading. Each navigation pays 500ms+ penalty.
|
1. **Replace `waitUntil: 'networkidle'` with `'domcontentloaded'` + targeted waits** — used ~20 times; `networkidle` worst-case for SPAs with persistent WebSocket + Leaflet tile loading. Each navigation pays 500ms+ penalty.
|
||||||
|
|
||||||
2. **Eliminate redundant navigations** — group tests by route; navigate once, run all assertions for that route.
|
2. **Eliminate redundant navigations** — group tests by route; navigate once, run all assertions for that route.
|
||||||
|
|
||||||
3. **Cache Playwright browser install in CI** — `npx playwright install chromium --with-deps` runs every frontend push. Self-hosted runner should retain browser between runs.
|
3. **Cache Playwright browser install in CI** — `npx playwright install chromium --with-deps` runs every frontend push. Self-hosted runner should retain browser between runs.
|
||||||
|
|
||||||
#### MEDIUM impact (10-30%)
|
#### MEDIUM impact (10-30%)
|
||||||
|
|
||||||
4. **Replace hardcoded `waitForTimeout` with event-driven waits** — ~17s scattered. Replace with `waitForSelector`, `waitForFunction`, or `page.waitForResponse`.
|
4. **Replace hardcoded `waitForTimeout` with event-driven waits** — ~17s scattered. Replace with `waitForSelector`, `waitForFunction`, or `page.waitForResponse`.
|
||||||
|
|
||||||
5. **Merge coverage collection into E2E run** — `collect-frontend-coverage.js` launches second browser. Extract `window.__coverage__` at E2E end instead.
|
5. **Merge coverage collection into E2E run** — `collect-frontend-coverage.js` launches second browser. Extract `window.__coverage__` at E2E end instead.
|
||||||
|
|
||||||
6. **Replace `sleep 5` server startup with health-check polling** — Start tests as soon as `/api/stats` responsive (~1-2s savings).
|
6. **Replace `sleep 5` server startup with health-check polling** — Start tests as soon as `/api/stats` responsive (~1-2s savings).
|
||||||
|
|
||||||
#### LOW impact (<10%)
|
#### LOW impact (<10%)
|
||||||
|
|
||||||
7. **Block unnecessary resources for non-visual tests** — use `page.route()` to abort map tiles, fonts.
|
7. **Block unnecessary resources for non-visual tests** — use `page.route()` to abort map tiles, fonts.
|
||||||
|
|
||||||
8. **Reduce default timeout 15s → 10s** — sufficient for local CI.
|
8. **Reduce default timeout 15s → 10s** — sufficient for local CI.
|
||||||
|
|
||||||
### Implementation notes
|
### Implementation notes
|
||||||
|
|
||||||
- Items 1-2 are test-file-only (Bishop/Newt scope)
|
- Items 1-2 are test-file-only (Bishop/Newt scope)
|
||||||
- Items 3, 5-6 are CI pipeline (Hicks scope)
|
- Items 3, 5-6 are CI pipeline (Hicks scope)
|
||||||
- No architectural changes; all incremental
|
- No architectural changes; all incremental
|
||||||
- All assertions remain identical — only wait strategies change
|
- All assertions remain identical — only wait strategies change
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2026-03-27T20:56:00Z — Protobuf API Contract (Merged)
|
### 2026-03-27T20:56:00Z — Protobuf API Contract (Merged)
|
||||||
**By:** Kpa-clawbot (via Copilot)
|
**By:** Kpa-clawbot (via Copilot)
|
||||||
**Decision:**
|
**Decision:**
|
||||||
1. All frontend/backend interfaces get protobuf definitions as single source of truth
|
1. All frontend/backend interfaces get protobuf definitions as single source of truth
|
||||||
2. Go generates structs with JSON tags from protos; Node stays unchanged — protos derived from Node's current JSON shapes
|
2. Go generates structs with JSON tags from protos; Node stays unchanged — protos derived from Node's current JSON shapes
|
||||||
3. Proto definitions MUST use inheritance and composition (no repeating field definitions)
|
3. Proto definitions MUST use inheritance and composition (no repeating field definitions)
|
||||||
4. Data flow: SQLite → proto struct → JSON; JSON blobs from DB deserialize against proto structs for validation
|
4. Data flow: SQLite → proto struct → JSON; JSON blobs from DB deserialize against proto structs for validation
|
||||||
5. CI pipeline's proto fixture capture runs against prod (stable reference), not staging
|
5. CI pipeline's proto fixture capture runs against prod (stable reference), not staging
|
||||||
|
|
||||||
**Rationale:** Eliminates parity bugs between Node and Go. Compiler-enforced contract. Prod is known-good baseline.
|
**Rationale:** Eliminates parity bugs between Node and Go. Compiler-enforced contract. Prod is known-good baseline.
|
||||||
|
|||||||
@@ -1,86 +1,86 @@
|
|||||||
# Spawn Batch — Proto Validation & Typed API Contracts
|
# Spawn Batch — Proto Validation & Typed API Contracts
|
||||||
|
|
||||||
**Timestamp:** 2026-03-27T22:19:53Z
|
**Timestamp:** 2026-03-27T22:19:53Z
|
||||||
**Scribe:** Orchestration Log Entry
|
**Scribe:** Orchestration Log Entry
|
||||||
**Scope:** Go server proto validation, fixture capture, CI architecture
|
**Scope:** Go server proto validation, fixture capture, CI architecture
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Team Accomplishments (Spawn Manifest)
|
## Team Accomplishments (Spawn Manifest)
|
||||||
|
|
||||||
### Hicks (Backend Dev)
|
### Hicks (Backend Dev)
|
||||||
- **Fixed #163:** 15 API violations — type mismatches in route handlers
|
- **Fixed #163:** 15 API violations — type mismatches in route handlers
|
||||||
- **Fixed #164:** 24 proto mismatches — shape inconsistencies between Node.js JSON and Go structs
|
- **Fixed #164:** 24 proto mismatches — shape inconsistencies between Node.js JSON and Go structs
|
||||||
- **Delivered:** `types.go` — 80 typed Go structs replacing all `map[string]interface{}` in route handlers
|
- **Delivered:** `types.go` — 80 typed Go structs replacing all `map[string]interface{}` in route handlers
|
||||||
- **Impact:** Proto contract fully wired into Go server; compiler now enforces API response shapes
|
- **Impact:** Proto contract fully wired into Go server; compiler now enforces API response shapes
|
||||||
|
|
||||||
### Bishop (Proto Validation)
|
### Bishop (Proto Validation)
|
||||||
- **Validated:** All proto definitions (0 errors)
|
- **Validated:** All proto definitions (0 errors)
|
||||||
- **Captured:** 33 Node.js API response fixtures from production
|
- **Captured:** 33 Node.js API response fixtures from production
|
||||||
- **Status:** Baseline fixture set ready for CI contract testing
|
- **Status:** Baseline fixture set ready for CI contract testing
|
||||||
|
|
||||||
### Hudson (CI/DevOps)
|
### Hudson (CI/DevOps)
|
||||||
- **Implemented:** CI proto validation pipeline with all 33 fixtures
|
- **Implemented:** CI proto validation pipeline with all 33 fixtures
|
||||||
- **Fixed:** Fixture capture source changed from staging → production
|
- **Fixed:** Fixture capture source changed from staging → production
|
||||||
- **Improved:** CI split into parallel tracks (backend tests, frontend tests, proto validation)
|
- **Improved:** CI split into parallel tracks (backend tests, frontend tests, proto validation)
|
||||||
- **Impact:** Proto contracts now validated against prod on every push
|
- **Impact:** Proto contracts now validated against prod on every push
|
||||||
|
|
||||||
### Coordinator
|
### Coordinator
|
||||||
- **Fixed:** Fixture capture source (staging → prod)
|
- **Fixed:** Fixture capture source (staging → prod)
|
||||||
- **Verified:** Data integrity of captured fixtures
|
- **Verified:** Data integrity of captured fixtures
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Key Milestone: Proto-Enforced API Contract
|
## Key Milestone: Proto-Enforced API Contract
|
||||||
|
|
||||||
**Status:** ✅ Complete
|
**Status:** ✅ Complete
|
||||||
|
|
||||||
Go server now has:
|
Go server now has:
|
||||||
1. Full type safety (80 structs replacing all `map[string]interface{}`)
|
1. Full type safety (80 structs replacing all `map[string]interface{}`)
|
||||||
2. Proto definitions as single source of truth
|
2. Proto definitions as single source of truth
|
||||||
3. Compiler-enforced JSON field matching (no more mismatches)
|
3. Compiler-enforced JSON field matching (no more mismatches)
|
||||||
4. CI validation on every push (all 33 fixtures + 0 errors)
|
4. CI validation on every push (all 33 fixtures + 0 errors)
|
||||||
|
|
||||||
**What Changed:**
|
**What Changed:**
|
||||||
- All route handlers return typed structs (proto-derived)
|
- All route handlers return typed structs (proto-derived)
|
||||||
- Response shapes match Node.js JSON exactly
|
- Response shapes match Node.js JSON exactly
|
||||||
- Any shape mismatch caught at compile time, not test time
|
- Any shape mismatch caught at compile time, not test time
|
||||||
|
|
||||||
**Frontend Impact:** None — JSON shapes unchanged, frontend code continues unchanged.
|
**Frontend Impact:** None — JSON shapes unchanged, frontend code continues unchanged.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Decisions Merged
|
## Decisions Merged
|
||||||
|
|
||||||
**New inbox entries processed:**
|
**New inbox entries processed:**
|
||||||
1. ✅ `copilot-directive-protobuf-contract.md` → decisions.md (1 decision)
|
1. ✅ `copilot-directive-protobuf-contract.md` → decisions.md (1 decision)
|
||||||
2. ✅ `copilot-directive-fixtures-from-prod.md` → decisions.md (1 directive)
|
2. ✅ `copilot-directive-fixtures-from-prod.md` → decisions.md (1 directive)
|
||||||
|
|
||||||
**Deduplication:** Both entries new (timestamps 2026-03-27T20:56:00Z, 2026-03-27T22:00:00Z). No duplicates detected.
|
**Deduplication:** Both entries new (timestamps 2026-03-27T20:56:00Z, 2026-03-27T22:00:00Z). No duplicates detected.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Decisions File Status
|
## Decisions File Status
|
||||||
|
|
||||||
**Location:** `.squad/decisions/decisions.md`
|
**Location:** `.squad/decisions/decisions.md`
|
||||||
**Current Size:** ~380 lines
|
**Current Size:** ~380 lines
|
||||||
**Archival Threshold:** 20KB
|
**Archival Threshold:** 20KB
|
||||||
**Status:** ✅ Well under threshold, no archival needed
|
**Status:** ✅ Well under threshold, no archival needed
|
||||||
|
|
||||||
**Sections:**
|
**Sections:**
|
||||||
1. User Directives (6 decisions)
|
1. User Directives (6 decisions)
|
||||||
2. Technical Fixes (7 issues)
|
2. Technical Fixes (7 issues)
|
||||||
3. Infrastructure & Deployment (3 decisions)
|
3. Infrastructure & Deployment (3 decisions)
|
||||||
4. Go Rewrite — API & Storage (7 decisions, +2 proto entries)
|
4. Go Rewrite — API & Storage (7 decisions, +2 proto entries)
|
||||||
5. E2E Playwright Performance (1 proposed strategy)
|
5. E2E Playwright Performance (1 proposed strategy)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
**Inbox Merged:** 2 entries → decisions.md
|
**Inbox Merged:** 2 entries → decisions.md
|
||||||
**Orchestration Log:** 1 new entry (this file)
|
**Orchestration Log:** 1 new entry (this file)
|
||||||
**Files Modified:** `.squad/decisions/decisions.md`
|
**Files Modified:** `.squad/decisions/decisions.md`
|
||||||
**Git Status:** Ready for commit
|
**Git Status:** Ready for commit
|
||||||
|
|
||||||
**Next Action:** Git commit with explicit file list (no `-A` flag).
|
**Next Action:** Git commit with explicit file list (no `-A` flag).
|
||||||
|
|||||||
@@ -1,178 +1,178 @@
|
|||||||
# Scribe Orchestration Log
|
# Scribe Orchestration Log
|
||||||
|
|
||||||
## 2026-03-27 — Session Summary & Finalization
|
## 2026-03-27 — Session Summary & Finalization
|
||||||
|
|
||||||
**Agent:** Scribe (Logging)
|
**Agent:** Scribe (Logging)
|
||||||
**Date:** 2026-03-27
|
**Date:** 2026-03-27
|
||||||
**Task:** Merge decision inbox, write session orchestration log entry, commit .squad/ changes
|
**Task:** Merge decision inbox, write session orchestration log entry, commit .squad/ changes
|
||||||
|
|
||||||
### Inbox Merge Status
|
### Inbox Merge Status
|
||||||
|
|
||||||
**Decision Inbox Review:** `.squad/decisions/inbox/` directory scanned — **EMPTY** (no new decisions filed during this session).
|
**Decision Inbox Review:** `.squad/decisions/inbox/` directory scanned — **EMPTY** (no new decisions filed during this session).
|
||||||
|
|
||||||
**Decisions.md Status:** Current file contains 9 decision categories:
|
**Decisions.md Status:** Current file contains 9 decision categories:
|
||||||
1. User Directives (6 decisions)
|
1. User Directives (6 decisions)
|
||||||
2. Technical Fixes (4 issues: #126, #133 parts 1-2, #123, #130, #131, #129, #132)
|
2. Technical Fixes (4 issues: #126, #133 parts 1-2, #123, #130, #131, #129, #132)
|
||||||
3. Infrastructure & Deployment (3 decisions: DB merge, Docker volumes, staging setup)
|
3. Infrastructure & Deployment (3 decisions: DB merge, Docker volumes, staging setup)
|
||||||
4. Go Rewrite — API & Storage (4 decisions: MQTT ingestor, web server, API parity, observation RAM optimization)
|
4. Go Rewrite — API & Storage (4 decisions: MQTT ingestor, web server, API parity, observation RAM optimization)
|
||||||
5. E2E Playwright Performance (proposed, not yet implemented)
|
5. E2E Playwright Performance (proposed, not yet implemented)
|
||||||
|
|
||||||
**No merges required** — all work captured in existing decision log categories.
|
**No merges required** — all work captured in existing decision log categories.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Session Orchestration Summary
|
## Session Orchestration Summary
|
||||||
|
|
||||||
**Session Scope:** #151-160 issues + Go rewrite staging + database merge + E2E expansion
|
**Session Scope:** #151-160 issues + Go rewrite staging + database merge + E2E expansion
|
||||||
|
|
||||||
### Agent Deliverables (28 issues closed)
|
### Agent Deliverables (28 issues closed)
|
||||||
|
|
||||||
#### Hicks (Backend Dev)
|
#### Hicks (Backend Dev)
|
||||||
- **Issues Fixed:** #123 (channel hash), #126 (hop prefixes), #133 (phantom nodes × 3), #143 (perf dashboard), #154-#155 (Go server parity)
|
- **Issues Fixed:** #123 (channel hash), #126 (hop prefixes), #133 (phantom nodes × 3), #143 (perf dashboard), #154-#155 (Go server parity)
|
||||||
- **Go Ingestor:** ~800 lines, 25 tests ✅ — MQTT ingestion, packet decode, DB writes
|
- **Go Ingestor:** ~800 lines, 25 tests ✅ — MQTT ingestion, packet decode, DB writes
|
||||||
- **Go Server:** ~2000 lines, 42 tests ✅ — REST API (35+ endpoints), WebSocket, SQLite polling
|
- **Go Server:** ~2000 lines, 42 tests ✅ — REST API (35+ endpoints), WebSocket, SQLite polling
|
||||||
- **API Parity:** All endpoints matching Node.js shape, transmission-centric queries, field fixes
|
- **API Parity:** All endpoints matching Node.js shape, transmission-centric queries, field fixes
|
||||||
- **Performance:** 8s → <100ms on `/api/packets?groupByHash=true`
|
- **Performance:** 8s → <100ms on `/api/packets?groupByHash=true`
|
||||||
- **Testing:** Backend coverage 85%+, all tests passing
|
- **Testing:** Backend coverage 85%+, all tests passing
|
||||||
|
|
||||||
#### Newt (Frontend)
|
#### Newt (Frontend)
|
||||||
- **Issues Fixed:** #130 (live map stale dimming), #131 (WS auto-update), #129 (observer comparison), #133 (live page pruning)
|
- **Issues Fixed:** #130 (live map stale dimming), #131 (WS auto-update), #129 (observer comparison), #133 (live page pruning)
|
||||||
- **Frontend Patterns:** WS cache reset (null + invalidateApiCache + re-fetch), detail pane CSS collapse, time-based eviction
|
- **Frontend Patterns:** WS cache reset (null + invalidateApiCache + re-fetch), detail pane CSS collapse, time-based eviction
|
||||||
- **Observer Comparison:** New `#/compare` route, pure function `comparePacketSets()` exposed on window
|
- **Observer Comparison:** New `#/compare` route, pure function `comparePacketSets()` exposed on window
|
||||||
- **E2E:** Playwright tests verified all routes, live page behavior, observer analytics
|
- **E2E:** Playwright tests verified all routes, live page behavior, observer analytics
|
||||||
- **Cache Busters:** Bumped in same commit as code changes
|
- **Cache Busters:** Bumped in same commit as code changes
|
||||||
|
|
||||||
#### Bishop (Tester)
|
#### Bishop (Tester)
|
||||||
- **PR Reviews:** Approved Hicks #6 + Newt #5 + Hudson DB merge plan with gap coverage
|
- **PR Reviews:** Approved Hicks #6 + Newt #5 + Hudson DB merge plan with gap coverage
|
||||||
- **Gap Coverage:** 14 phantom node tests, 5 WS handler tests added to backend suite
|
- **Gap Coverage:** 14 phantom node tests, 5 WS handler tests added to backend suite
|
||||||
- **E2E Expansion:** 16 → 42 Playwright tests covering 11 routes + new audio lab, channels, observers, traces, perf pages
|
- **E2E Expansion:** 16 → 42 Playwright tests covering 11 routes + new audio lab, channels, observers, traces, perf pages
|
||||||
- **Coverage Validation:** Frontend 42%+, backend 85%+ (both on target)
|
- **Coverage Validation:** Frontend 42%+, backend 85%+ (both on target)
|
||||||
- **Outcome:** 526 backend tests + 42 E2E tests, all passing ✅
|
- **Outcome:** 526 backend tests + 42 E2E tests, all passing ✅
|
||||||
|
|
||||||
#### Kobayashi (Lead)
|
#### Kobayashi (Lead)
|
||||||
- **Root Cause Analysis:** Issue #133 phantom node creation traced to `autoLearnHopNodes()` with `hash_size=1`
|
- **Root Cause Analysis:** Issue #133 phantom node creation traced to `autoLearnHopNodes()` with `hash_size=1`
|
||||||
- **DB Merge Plan:** 6-phase strategy (pre-flight, backup, merge, deploy, validate, cleanup) with dedup logic
|
- **DB Merge Plan:** 6-phase strategy (pre-flight, backup, merge, deploy, validate, cleanup) with dedup logic
|
||||||
- **Coordination:** Assigned fix owners, reviewed 6 PRs, approved DB merge execution
|
- **Coordination:** Assigned fix owners, reviewed 6 PRs, approved DB merge execution
|
||||||
- **Outcome:** 185MB staging DB → 51,723 transmissions + 1,237,186 observations merged successfully
|
- **Outcome:** 185MB staging DB → 51,723 transmissions + 1,237,186 observations merged successfully
|
||||||
|
|
||||||
#### Hudson (DevOps)
|
#### Hudson (DevOps)
|
||||||
- **Database Merge:** Executed production merge (0 data loss, ~2 min downtime, 8,491ms load time)
|
- **Database Merge:** Executed production merge (0 data loss, ~2 min downtime, 8,491ms load time)
|
||||||
- **Docker Compose:** Unified volume paths, reconciled manage.sh ↔ docker-compose.yml (no version key, v2 compatible)
|
- **Docker Compose:** Unified volume paths, reconciled manage.sh ↔ docker-compose.yml (no version key, v2 compatible)
|
||||||
- **Staging Setup:** Created `~/meshcore-staging-data/` with old problematic DB for debugging, separate MQTT/HTTP ports
|
- **Staging Setup:** Created `~/meshcore-staging-data/` with old problematic DB for debugging, separate MQTT/HTTP ports
|
||||||
- **CI Pipeline:** Auto-check `docker compose` install, staging auto-deploy with health checks, manual production promotion
|
- **CI Pipeline:** Auto-check `docker compose` install, staging auto-deploy with health checks, manual production promotion
|
||||||
- **Infrastructure:** Azure CLI user restoration, Docker group membership, backup retention (7 days)
|
- **Infrastructure:** Azure CLI user restoration, Docker group membership, backup retention (7 days)
|
||||||
- **Outcome:** Production stable (860MiB RSS post-merge), staging ready for Go server deployment (port 82)
|
- **Outcome:** Production stable (860MiB RSS post-merge), staging ready for Go server deployment (port 82)
|
||||||
|
|
||||||
#### Coordinator (Manual Triage)
|
#### Coordinator (Manual Triage)
|
||||||
- **Issue Closure:** 9 issues closed manually (#134-#142, duplicates + resolved UI polish)
|
- **Issue Closure:** 9 issues closed manually (#134-#142, duplicates + resolved UI polish)
|
||||||
- **New Issue:** #146 filed (unique node count bug — 6502 nodes caused by phantom cleanup audit gap)
|
- **New Issue:** #146 filed (unique node count bug — 6502 nodes caused by phantom cleanup audit gap)
|
||||||
- **Outcome:** Backlog cleaned, new issue scoped for Hicks backend audit
|
- **Outcome:** Backlog cleaned, new issue scoped for Hicks backend audit
|
||||||
|
|
||||||
#### Ripley (Support)
|
#### Ripley (Support)
|
||||||
- **Onboarding:** Joined as Support Engineer mid-session
|
- **Onboarding:** Joined as Support Engineer mid-session
|
||||||
- **Knowledge Transfer:** Explained staleness thresholds (24h companions/sensors, 72h infrastructure), 7-day active window, health calculations
|
- **Knowledge Transfer:** Explained staleness thresholds (24h companions/sensors, 72h infrastructure), 7-day active window, health calculations
|
||||||
- **Documentation Reference:** Pointed to `roles.js` as authoritative source for health thresholds
|
- **Documentation Reference:** Pointed to `roles.js` as authoritative source for health thresholds
|
||||||
- **Outcome:** Support engineer ready for operational questions and user escalations
|
- **Outcome:** Support engineer ready for operational questions and user escalations
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Orchestration Log Entries Written
|
## Orchestration Log Entries Written
|
||||||
|
|
||||||
All agent logs already present at session end:
|
All agent logs already present at session end:
|
||||||
- `bishop-2026-03-27.md` (116 lines) — PR reviews, gap coverage, E2E expansion
|
- `bishop-2026-03-27.md` (116 lines) — PR reviews, gap coverage, E2E expansion
|
||||||
- `hicks-2026-03-27.md` (102 lines) — 6 fixes, Go ingestor/server, API parity, perf dashboard
|
- `hicks-2026-03-27.md` (102 lines) — 6 fixes, Go ingestor/server, API parity, perf dashboard
|
||||||
- `newt-2026-03-27.md` (56 lines) — 4 frontend fixes, WS patterns, observer comparison
|
- `newt-2026-03-27.md` (56 lines) — 4 frontend fixes, WS patterns, observer comparison
|
||||||
- `kobayashi-2026-03-27.md` (27 lines) — Root cause analysis, DB merge plan, coordination
|
- `kobayashi-2026-03-27.md` (27 lines) — Root cause analysis, DB merge plan, coordination
|
||||||
- `hudson-2026-03-27.md` (117 lines) — DB merge execution, Docker Compose migration, staging setup, CI pipeline
|
- `hudson-2026-03-27.md` (117 lines) — DB merge execution, Docker Compose migration, staging setup, CI pipeline
|
||||||
- `ripley-2026-03-27.md` (30 lines) — Support onboarding, health threshold documentation
|
- `ripley-2026-03-27.md` (30 lines) — Support onboarding, health threshold documentation
|
||||||
|
|
||||||
**Entry Total:** 448 lines of orchestration logs covering 28 issues, 2 Go services, database merge, staging deployment, CI pipeline updates, 42 E2E tests, 19 backend fixes
|
**Entry Total:** 448 lines of orchestration logs covering 28 issues, 2 Go services, database merge, staging deployment, CI pipeline updates, 42 E2E tests, 19 backend fixes
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Decisions.md Review
|
## Decisions.md Review
|
||||||
|
|
||||||
Current decisions.md (342 lines) contains authoritative log of all technical + infrastructure + deployment decisions made during #151-160 session. No archival needed (well under 20KB threshold). Organized by:
|
Current decisions.md (342 lines) contains authoritative log of all technical + infrastructure + deployment decisions made during #151-160 session. No archival needed (well under 20KB threshold). Organized by:
|
||||||
1. User Directives (process decisions)
|
1. User Directives (process decisions)
|
||||||
2. Technical Fixes (bug fixes with rationale)
|
2. Technical Fixes (bug fixes with rationale)
|
||||||
3. Infrastructure & Deployment (ops decisions)
|
3. Infrastructure & Deployment (ops decisions)
|
||||||
4. Go Rewrite — API & Storage (architecture decisions)
|
4. Go Rewrite — API & Storage (architecture decisions)
|
||||||
5. E2E Playwright Performance (performance optimization strategy)
|
5. E2E Playwright Performance (performance optimization strategy)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Git Status
|
## Git Status
|
||||||
|
|
||||||
Scribe operations:
|
Scribe operations:
|
||||||
- ✅ No inbox → decisions.md merges (inbox empty)
|
- ✅ No inbox → decisions.md merges (inbox empty)
|
||||||
- ✅ Orchestration logs written (6 agent logs, 448 lines)
|
- ✅ Orchestration logs written (6 agent logs, 448 lines)
|
||||||
- ✅ Session summary complete
|
- ✅ Session summary complete
|
||||||
- ✅ No modifications to non-.squad/ files
|
- ✅ No modifications to non-.squad/ files
|
||||||
- ✅ Ready for commit
|
- ✅ Ready for commit
|
||||||
|
|
||||||
### .squad/ Directory Structure
|
### .squad/ Directory Structure
|
||||||
```
|
```
|
||||||
.squad/
|
.squad/
|
||||||
├── agents/
|
├── agents/
|
||||||
│ ├── bishop/
|
│ ├── bishop/
|
||||||
│ ├── hicks/
|
│ ├── hicks/
|
||||||
│ ├── kobayashi/
|
│ ├── kobayashi/
|
||||||
│ ├── newt/
|
│ ├── newt/
|
||||||
│ ├── ripley/
|
│ ├── ripley/
|
||||||
│ ├── hudson/
|
│ ├── hudson/
|
||||||
│ └── coordinator/
|
│ └── coordinator/
|
||||||
├── decisions/
|
├── decisions/
|
||||||
│ ├── decisions.md (342 lines, final)
|
│ ├── decisions.md (342 lines, final)
|
||||||
│ └── inbox/ (empty)
|
│ └── inbox/ (empty)
|
||||||
├── orchestration-log/
|
├── orchestration-log/
|
||||||
│ ├── bishop-2026-03-27.md
|
│ ├── bishop-2026-03-27.md
|
||||||
│ ├── hicks-2026-03-27.md
|
│ ├── hicks-2026-03-27.md
|
||||||
│ ├── newt-2026-03-27.md
|
│ ├── newt-2026-03-27.md
|
||||||
│ ├── kobayashi-2026-03-27.md
|
│ ├── kobayashi-2026-03-27.md
|
||||||
│ ├── hudson-2026-03-27.md
|
│ ├── hudson-2026-03-27.md
|
||||||
│ ├── ripley-2026-03-27.md
|
│ ├── ripley-2026-03-27.md
|
||||||
│ └── scribe-2026-03-27.md ← NEW
|
│ └── scribe-2026-03-27.md ← NEW
|
||||||
├── log/ (session artifacts)
|
├── log/ (session artifacts)
|
||||||
└── agents/scribe/charter.md
|
└── agents/scribe/charter.md
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Session Impact Summary
|
## Session Impact Summary
|
||||||
|
|
||||||
| Metric | Before | After | Status |
|
| Metric | Before | After | Status |
|
||||||
|--------|--------|-------|--------|
|
|--------|--------|-------|--------|
|
||||||
| **Issues Closed** | Open backlog | 28 closed | ✅ |
|
| **Issues Closed** | Open backlog | 28 closed | ✅ |
|
||||||
| **Node Count** | 7,308 (phantom) | ~400 (7-day active) | ✅ Fixed |
|
| **Node Count** | 7,308 (phantom) | ~400 (7-day active) | ✅ Fixed |
|
||||||
| **Heap Usage** | 2.7GB (OOM risk) | 860MB RSS | ✅ Fixed |
|
| **Heap Usage** | 2.7GB (OOM risk) | 860MB RSS | ✅ Fixed |
|
||||||
| **Prod DB Size** | 21MB | 206MB (merged) | ✅ Complete |
|
| **Prod DB Size** | 21MB | 206MB (merged) | ✅ Complete |
|
||||||
| **Transmissions** | 46K | 51,723 | ✅ Complete |
|
| **Transmissions** | 46K | 51,723 | ✅ Complete |
|
||||||
| **Observations** | ~50K | 1,237,186 | ✅ Complete |
|
| **Observations** | ~50K | 1,237,186 | ✅ Complete |
|
||||||
| **Go MQTT Ingestor** | Non-existent | 25 tests ✅ | ✅ Delivered |
|
| **Go MQTT Ingestor** | Non-existent | 25 tests ✅ | ✅ Delivered |
|
||||||
| **Go Web Server** | Non-existent | 42 tests ✅ | ✅ Delivered |
|
| **Go Web Server** | Non-existent | 42 tests ✅ | ✅ Delivered |
|
||||||
| **E2E Test Coverage** | 16 tests | 42 tests | ✅ Expanded |
|
| **E2E Test Coverage** | 16 tests | 42 tests | ✅ Expanded |
|
||||||
| **Backend Test Coverage** | 80%+ | 85%+ | ✅ Improved |
|
| **Backend Test Coverage** | 80%+ | 85%+ | ✅ Improved |
|
||||||
| **Frontend Test Coverage** | 38%+ | 42%+ | ✅ Improved |
|
| **Frontend Test Coverage** | 38%+ | 42%+ | ✅ Improved |
|
||||||
| **Staging Environment** | Non-existent | Docker Compose + Go-ready | ✅ Delivered |
|
| **Staging Environment** | Non-existent | Docker Compose + Go-ready | ✅ Delivered |
|
||||||
| **API Parity** | Node.js only | Go server 100% match | ✅ Complete |
|
| **API Parity** | Node.js only | Go server 100% match | ✅ Complete |
|
||||||
| **Production Uptime** | Pre-merge | Post-merge stable | ✅ Restored |
|
| **Production Uptime** | Pre-merge | Post-merge stable | ✅ Restored |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Outcome
|
## Outcome
|
||||||
|
|
||||||
✅ **Session Complete**
|
✅ **Session Complete**
|
||||||
|
|
||||||
- All 28 issues closed
|
- All 28 issues closed
|
||||||
- Go MQTT ingestor + web server deployed to staging (ready for Go runtime performance validation)
|
- Go MQTT ingestor + web server deployed to staging (ready for Go runtime performance validation)
|
||||||
- Database merge successful (0 data loss, minimal downtime)
|
- Database merge successful (0 data loss, minimal downtime)
|
||||||
- Staging environment operational (Docker Compose, old DB for debugging)
|
- Staging environment operational (Docker Compose, old DB for debugging)
|
||||||
- E2E test coverage expanded (16 → 42 tests)
|
- E2E test coverage expanded (16 → 42 tests)
|
||||||
- Backend test coverage target met (85%+)
|
- Backend test coverage target met (85%+)
|
||||||
- Production restored to healthy state (860MB RSS, no phantom nodes)
|
- Production restored to healthy state (860MB RSS, no phantom nodes)
|
||||||
- CI pipeline auto-heals (Docker Compose v2 check)
|
- CI pipeline auto-heals (Docker Compose v2 check)
|
||||||
- All agent logs written to orchestration-log/
|
- All agent logs written to orchestration-log/
|
||||||
- Decisions.md current and comprehensive
|
- Decisions.md current and comprehensive
|
||||||
- Ready for final git commit
|
- Ready for final git commit
|
||||||
|
|
||||||
**Status:** 🟢 READY FOR COMMIT
|
**Status:** 🟢 READY FOR COMMIT
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
# Work Routing
|
# Work Routing
|
||||||
|
|
||||||
How to decide who handles what.
|
How to decide who handles what.
|
||||||
|
|
||||||
## Routing Table
|
## Routing Table
|
||||||
|
|
||||||
| Work Type | Route To | Examples |
|
| Work Type | Route To | Examples |
|
||||||
|-----------|----------|----------|
|
|-----------|----------|----------|
|
||||||
| Architecture, scope, decisions | Kobayashi | Feature planning, trade-offs, scope decisions |
|
| Architecture, scope, decisions | Kobayashi | Feature planning, trade-offs, scope decisions |
|
||||||
| Code review, PR review | Kobayashi | Review PRs, check quality, approve/reject |
|
| Code review, PR review | Kobayashi | Review PRs, check quality, approve/reject |
|
||||||
| server.js, API routes, Express | Hicks | Add endpoints, fix API bugs, MQTT config |
|
| server.js, API routes, Express | Hicks | Add endpoints, fix API bugs, MQTT config |
|
||||||
| decoder.js, packet parsing | Hicks | Protocol changes, parser bugs, new packet types |
|
| decoder.js, packet parsing | Hicks | Protocol changes, parser bugs, new packet types |
|
||||||
| packet-store.js, db.js, SQLite | Hicks | Storage bugs, query optimization, schema changes |
|
| packet-store.js, db.js, SQLite | Hicks | Storage bugs, query optimization, schema changes |
|
||||||
| server-helpers.js, MQTT, WebSocket | Hicks | Helper functions, real-time data flow |
|
| server-helpers.js, MQTT, WebSocket | Hicks | Helper functions, real-time data flow |
|
||||||
| Performance optimization | Hicks | Caching, O(n) improvements, response times |
|
| Performance optimization | Hicks | Caching, O(n) improvements, response times |
|
||||||
| Docker, deployment, manage.sh | Hicks | Container config, deploy scripts |
|
| Docker, deployment, manage.sh | Hicks | Container config, deploy scripts |
|
||||||
| MeshCore protocol/firmware | Hicks | Read firmware source, verify protocol behavior |
|
| MeshCore protocol/firmware | Hicks | Read firmware source, verify protocol behavior |
|
||||||
| public/*.js (all frontend modules) | Newt | UI features, interactions, SPA routing |
|
| public/*.js (all frontend modules) | Newt | UI features, interactions, SPA routing |
|
||||||
| Leaflet maps, live visualization | Newt | Map markers, VCR playback, animations |
|
| Leaflet maps, live visualization | Newt | Map markers, VCR playback, animations |
|
||||||
| CSS, theming, customize.js | Newt | Styles, CSS variables, theme customizer |
|
| CSS, theming, customize.js | Newt | Styles, CSS variables, theme customizer |
|
||||||
| packet-filter.js (filter engine) | Newt | Filter syntax, parser, Wireshark-style queries |
|
| packet-filter.js (filter engine) | Newt | Filter syntax, parser, Wireshark-style queries |
|
||||||
| index.html, cache busters | Newt | Script tags, version bumps |
|
| index.html, cache busters | Newt | Script tags, version bumps |
|
||||||
| Unit tests, test-*.js | Bishop | Write/fix tests, coverage improvements |
|
| Unit tests, test-*.js | Bishop | Write/fix tests, coverage improvements |
|
||||||
| Playwright E2E tests | Bishop | Browser tests, UI verification |
|
| Playwright E2E tests | Bishop | Browser tests, UI verification |
|
||||||
| Coverage, CI pipeline | Bishop | Coverage targets, CI config |
|
| Coverage, CI pipeline | Bishop | Coverage targets, CI config |
|
||||||
| CI/CD pipeline, .github/workflows | Hudson | Pipeline config, step optimization, CI debugging |
|
| CI/CD pipeline, .github/workflows | Hudson | Pipeline config, step optimization, CI debugging |
|
||||||
| Docker, Dockerfile, docker/ | Hudson | Container config, build optimization |
|
| Docker, Dockerfile, docker/ | Hudson | Container config, build optimization |
|
||||||
| manage.sh, deployment scripts | Hudson | Deploy scripts, server management |
|
| manage.sh, deployment scripts | Hudson | Deploy scripts, server management |
|
||||||
| scripts/, coverage tooling | Hudson | Build scripts, coverage collector optimization |
|
| scripts/, coverage tooling | Hudson | Build scripts, coverage collector optimization |
|
||||||
| Azure, VM, infrastructure | Hudson | az CLI, SSH, server provisioning, monitoring |
|
| Azure, VM, infrastructure | Hudson | az CLI, SSH, server provisioning, monitoring |
|
||||||
| Production debugging, DB ops | Hudson | SQLite recovery, WAL issues, process diagnostics |
|
| Production debugging, DB ops | Hudson | SQLite recovery, WAL issues, process diagnostics |
|
||||||
| User questions, "why does X..." | Ripley | Community support, UI behavior explanations |
|
| User questions, "why does X..." | Ripley | Community support, UI behavior explanations |
|
||||||
| Bug report triage from users | Ripley | Analyze reports, reproduce, route to dev |
|
| Bug report triage from users | Ripley | Analyze reports, reproduce, route to dev |
|
||||||
| GitHub issue comments (support) | Ripley | Explain behavior, suggest workarounds |
|
| GitHub issue comments (support) | Ripley | Explain behavior, suggest workarounds |
|
||||||
| README, docs/ | Kobayashi | Documentation updates |
|
| README, docs/ | Kobayashi | Documentation updates |
|
||||||
| Session logging | Scribe | Automatic — never needs routing |
|
| Session logging | Scribe | Automatic — never needs routing |
|
||||||
|
|
||||||
## Issue Routing
|
## Issue Routing
|
||||||
|
|
||||||
| Label | Action | Who |
|
| Label | Action | Who |
|
||||||
|-------|--------|-----|
|
|-------|--------|-----|
|
||||||
| `squad` | Triage: analyze issue, assign `squad:{member}` label | Lead |
|
| `squad` | Triage: analyze issue, assign `squad:{member}` label | Lead |
|
||||||
| `squad:{name}` | Pick up issue and complete the work | Named member |
|
| `squad:{name}` | Pick up issue and complete the work | Named member |
|
||||||
|
|
||||||
### How Issue Assignment Works
|
### How Issue Assignment Works
|
||||||
|
|
||||||
1. When a GitHub issue gets the `squad` label, the **Lead** triages it — analyzing content, assigning the right `squad:{member}` label, and commenting with triage notes.
|
1. When a GitHub issue gets the `squad` label, the **Lead** triages it — analyzing content, assigning the right `squad:{member}` label, and commenting with triage notes.
|
||||||
2. When a `squad:{member}` label is applied, that member picks up the issue in their next session.
|
2. When a `squad:{member}` label is applied, that member picks up the issue in their next session.
|
||||||
3. Members can reassign by removing their label and adding another member's label.
|
3. Members can reassign by removing their label and adding another member's label.
|
||||||
4. The `squad` label is the "inbox" — untriaged issues waiting for Lead review.
|
4. The `squad` label is the "inbox" — untriaged issues waiting for Lead review.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
1. **Eager by default** — spawn all agents who could usefully start work, including anticipatory downstream work.
|
1. **Eager by default** — spawn all agents who could usefully start work, including anticipatory downstream work.
|
||||||
2. **Scribe always runs** after substantial work, always as `mode: "background"`. Never blocks.
|
2. **Scribe always runs** after substantial work, always as `mode: "background"`. Never blocks.
|
||||||
3. **Quick facts → coordinator answers directly.** Don't spawn an agent for "what port does the server run on?"
|
3. **Quick facts → coordinator answers directly.** Don't spawn an agent for "what port does the server run on?"
|
||||||
4. **When two agents could handle it**, pick the one whose domain is the primary concern.
|
4. **When two agents could handle it**, pick the one whose domain is the primary concern.
|
||||||
5. **"Team, ..." → fan-out.** Spawn all relevant agents in parallel as `mode: "background"`.
|
5. **"Team, ..." → fan-out.** Spawn all relevant agents in parallel as `mode: "background"`.
|
||||||
6. **Anticipate downstream work.** If a feature is being built, spawn the tester to write test cases from requirements simultaneously.
|
6. **Anticipate downstream work.** If a feature is being built, spawn the tester to write test cases from requirements simultaneously.
|
||||||
7. **Issue-labeled work** — when a `squad:{member}` label is applied to an issue, route to that member. The Lead handles all `squad` (base label) triage.
|
7. **Issue-labeled work** — when a `squad:{member}` label is applied to an issue, route to that member. The Lead handles all `squad` (base label) triage.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"universe_usage_history": [],
|
"universe_usage_history": [],
|
||||||
"assignment_cast_snapshots": {}
|
"assignment_cast_snapshots": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
{
|
{
|
||||||
"casting_policy_version": "1.1",
|
"casting_policy_version": "1.1",
|
||||||
"allowlist_universes": [
|
"allowlist_universes": [
|
||||||
"The Usual Suspects",
|
"The Usual Suspects",
|
||||||
"Reservoir Dogs",
|
"Reservoir Dogs",
|
||||||
"Alien",
|
"Alien",
|
||||||
"Ocean's Eleven",
|
"Ocean's Eleven",
|
||||||
"Arrested Development",
|
"Arrested Development",
|
||||||
"Star Wars",
|
"Star Wars",
|
||||||
"The Matrix",
|
"The Matrix",
|
||||||
"Firefly",
|
"Firefly",
|
||||||
"The Goonies",
|
"The Goonies",
|
||||||
"The Simpsons",
|
"The Simpsons",
|
||||||
"Breaking Bad",
|
"Breaking Bad",
|
||||||
"Lost",
|
"Lost",
|
||||||
"Marvel Cinematic Universe",
|
"Marvel Cinematic Universe",
|
||||||
"DC Universe",
|
"DC Universe",
|
||||||
"Futurama"
|
"Futurama"
|
||||||
],
|
],
|
||||||
"universe_capacity": {
|
"universe_capacity": {
|
||||||
"The Usual Suspects": 6,
|
"The Usual Suspects": 6,
|
||||||
"Reservoir Dogs": 8,
|
"Reservoir Dogs": 8,
|
||||||
"Alien": 8,
|
"Alien": 8,
|
||||||
"Ocean's Eleven": 14,
|
"Ocean's Eleven": 14,
|
||||||
"Arrested Development": 15,
|
"Arrested Development": 15,
|
||||||
"Star Wars": 12,
|
"Star Wars": 12,
|
||||||
"The Matrix": 10,
|
"The Matrix": 10,
|
||||||
"Firefly": 10,
|
"Firefly": 10,
|
||||||
"The Goonies": 8,
|
"The Goonies": 8,
|
||||||
"The Simpsons": 20,
|
"The Simpsons": 20,
|
||||||
"Breaking Bad": 12,
|
"Breaking Bad": 12,
|
||||||
"Lost": 18,
|
"Lost": 18,
|
||||||
"Marvel Cinematic Universe": 25,
|
"Marvel Cinematic Universe": 25,
|
||||||
"DC Universe": 18,
|
"DC Universe": 18,
|
||||||
"Futurama": 12
|
"Futurama": 12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +1,104 @@
|
|||||||
# Casting Reference
|
# Casting Reference
|
||||||
|
|
||||||
On-demand reference for Squad's casting system. Loaded during Init Mode or when adding team members.
|
On-demand reference for Squad's casting system. Loaded during Init Mode or when adding team members.
|
||||||
|
|
||||||
## Universe Table
|
## Universe Table
|
||||||
|
|
||||||
| Universe | Capacity | Shape Tags | Resonance Signals |
|
| Universe | Capacity | Shape Tags | Resonance Signals |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| The Usual Suspects | 6 | small, noir, ensemble | crime, heist, mystery, deception |
|
| The Usual Suspects | 6 | small, noir, ensemble | crime, heist, mystery, deception |
|
||||||
| Reservoir Dogs | 8 | small, noir, ensemble | crime, heist, tension, loyalty |
|
| Reservoir Dogs | 8 | small, noir, ensemble | crime, heist, tension, loyalty |
|
||||||
| Alien | 8 | small, sci-fi, survival | space, isolation, threat, engineering |
|
| Alien | 8 | small, sci-fi, survival | space, isolation, threat, engineering |
|
||||||
| Ocean's Eleven | 14 | medium, heist, ensemble | planning, coordination, roles, charm |
|
| Ocean's Eleven | 14 | medium, heist, ensemble | planning, coordination, roles, charm |
|
||||||
| Arrested Development | 15 | medium, comedy, ensemble | dysfunction, business, family, satire |
|
| Arrested Development | 15 | medium, comedy, ensemble | dysfunction, business, family, satire |
|
||||||
| Star Wars | 12 | medium, sci-fi, epic | conflict, mentorship, legacy, rebellion |
|
| Star Wars | 12 | medium, sci-fi, epic | conflict, mentorship, legacy, rebellion |
|
||||||
| The Matrix | 10 | medium, sci-fi, cyberpunk | systems, reality, hacking, philosophy |
|
| The Matrix | 10 | medium, sci-fi, cyberpunk | systems, reality, hacking, philosophy |
|
||||||
| Firefly | 10 | medium, sci-fi, western | frontier, crew, independence, smuggling |
|
| Firefly | 10 | medium, sci-fi, western | frontier, crew, independence, smuggling |
|
||||||
| The Goonies | 8 | small, adventure, ensemble | exploration, treasure, kids, teamwork |
|
| The Goonies | 8 | small, adventure, ensemble | exploration, treasure, kids, teamwork |
|
||||||
| The Simpsons | 20 | large, comedy, ensemble | satire, community, family, absurdity |
|
| The Simpsons | 20 | large, comedy, ensemble | satire, community, family, absurdity |
|
||||||
| Breaking Bad | 12 | medium, drama, tension | chemistry, transformation, consequence, power |
|
| Breaking Bad | 12 | medium, drama, tension | chemistry, transformation, consequence, power |
|
||||||
| Lost | 18 | large, mystery, ensemble | survival, mystery, groups, leadership |
|
| Lost | 18 | large, mystery, ensemble | survival, mystery, groups, leadership |
|
||||||
| Marvel Cinematic Universe | 25 | large, action, ensemble | heroism, teamwork, powers, scale |
|
| Marvel Cinematic Universe | 25 | large, action, ensemble | heroism, teamwork, powers, scale |
|
||||||
| DC Universe | 18 | large, action, ensemble | justice, duality, powers, mythology |
|
| DC Universe | 18 | large, action, ensemble | justice, duality, powers, mythology |
|
||||||
| Futurama | 12 | medium, sci-fi, comedy | future, robots, space, absurdity |
|
| Futurama | 12 | medium, sci-fi, comedy | future, robots, space, absurdity |
|
||||||
|
|
||||||
**Total: 15 universes** — capacity range 6–25.
|
**Total: 15 universes** — capacity range 6–25.
|
||||||
|
|
||||||
## Selection Algorithm
|
## Selection Algorithm
|
||||||
|
|
||||||
Universe selection is deterministic. Score each universe and pick the highest:
|
Universe selection is deterministic. Score each universe and pick the highest:
|
||||||
|
|
||||||
```
|
```
|
||||||
score = size_fit + shape_fit + resonance_fit + LRU
|
score = size_fit + shape_fit + resonance_fit + LRU
|
||||||
```
|
```
|
||||||
|
|
||||||
| Factor | Description |
|
| Factor | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `size_fit` | How well the universe capacity matches the team size. Prefer universes where capacity ≥ agent_count with minimal waste. |
|
| `size_fit` | How well the universe capacity matches the team size. Prefer universes where capacity ≥ agent_count with minimal waste. |
|
||||||
| `shape_fit` | Match universe shape tags against the assignment shape derived from the project description. |
|
| `shape_fit` | Match universe shape tags against the assignment shape derived from the project description. |
|
||||||
| `resonance_fit` | Match universe resonance signals against session and repo context signals. |
|
| `resonance_fit` | Match universe resonance signals against session and repo context signals. |
|
||||||
| `LRU` | Least-recently-used bonus — prefer universes not used in recent assignments (from `history.json`). |
|
| `LRU` | Least-recently-used bonus — prefer universes not used in recent assignments (from `history.json`). |
|
||||||
|
|
||||||
Same inputs → same choice (unless LRU changes between assignments).
|
Same inputs → same choice (unless LRU changes between assignments).
|
||||||
|
|
||||||
## Casting State File Schemas
|
## Casting State File Schemas
|
||||||
|
|
||||||
### policy.json
|
### policy.json
|
||||||
|
|
||||||
Source template: `.squad/templates/casting-policy.json`
|
Source template: `.squad/templates/casting-policy.json`
|
||||||
Runtime location: `.squad/casting/policy.json`
|
Runtime location: `.squad/casting/policy.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"casting_policy_version": "1.1",
|
"casting_policy_version": "1.1",
|
||||||
"allowlist_universes": ["Universe Name", "..."],
|
"allowlist_universes": ["Universe Name", "..."],
|
||||||
"universe_capacity": {
|
"universe_capacity": {
|
||||||
"Universe Name": 10
|
"Universe Name": 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### registry.json
|
### registry.json
|
||||||
|
|
||||||
Source template: `.squad/templates/casting-registry.json`
|
Source template: `.squad/templates/casting-registry.json`
|
||||||
Runtime location: `.squad/casting/registry.json`
|
Runtime location: `.squad/casting/registry.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"agents": {
|
"agents": {
|
||||||
"agent-role-id": {
|
"agent-role-id": {
|
||||||
"persistent_name": "CharacterName",
|
"persistent_name": "CharacterName",
|
||||||
"universe": "Universe Name",
|
"universe": "Universe Name",
|
||||||
"created_at": "ISO-8601",
|
"created_at": "ISO-8601",
|
||||||
"legacy_named": false,
|
"legacy_named": false,
|
||||||
"status": "active"
|
"status": "active"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### history.json
|
### history.json
|
||||||
|
|
||||||
Source template: `.squad/templates/casting-history.json`
|
Source template: `.squad/templates/casting-history.json`
|
||||||
Runtime location: `.squad/casting/history.json`
|
Runtime location: `.squad/casting/history.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"universe_usage_history": [
|
"universe_usage_history": [
|
||||||
{
|
{
|
||||||
"universe": "Universe Name",
|
"universe": "Universe Name",
|
||||||
"assignment_id": "unique-id",
|
"assignment_id": "unique-id",
|
||||||
"used_at": "ISO-8601"
|
"used_at": "ISO-8601"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"assignment_cast_snapshots": {
|
"assignment_cast_snapshots": {
|
||||||
"assignment-id": {
|
"assignment-id": {
|
||||||
"universe": "Universe Name",
|
"universe": "Universe Name",
|
||||||
"agents": {
|
"agents": {
|
||||||
"role-id": "CharacterName"
|
"role-id": "CharacterName"
|
||||||
},
|
},
|
||||||
"created_at": "ISO-8601"
|
"created_at": "ISO-8601"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"agents": {}
|
"agents": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
[
|
[
|
||||||
"Fry",
|
"Fry",
|
||||||
"Leela",
|
"Leela",
|
||||||
"Bender",
|
"Bender",
|
||||||
"Farnsworth",
|
"Farnsworth",
|
||||||
"Zoidberg",
|
"Zoidberg",
|
||||||
"Amy",
|
"Amy",
|
||||||
"Zapp",
|
"Zapp",
|
||||||
"Kif"
|
"Kif"
|
||||||
]
|
]
|
||||||
@@ -1,41 +1,41 @@
|
|||||||
# Ceremonies
|
# Ceremonies
|
||||||
|
|
||||||
> Team meetings that happen before or after work. Each squad configures their own.
|
> Team meetings that happen before or after work. Each squad configures their own.
|
||||||
|
|
||||||
## Design Review
|
## Design Review
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|-------|-------|
|
|-------|-------|
|
||||||
| **Trigger** | auto |
|
| **Trigger** | auto |
|
||||||
| **When** | before |
|
| **When** | before |
|
||||||
| **Condition** | multi-agent task involving 2+ agents modifying shared systems |
|
| **Condition** | multi-agent task involving 2+ agents modifying shared systems |
|
||||||
| **Facilitator** | lead |
|
| **Facilitator** | lead |
|
||||||
| **Participants** | all-relevant |
|
| **Participants** | all-relevant |
|
||||||
| **Time budget** | focused |
|
| **Time budget** | focused |
|
||||||
| **Enabled** | ✅ yes |
|
| **Enabled** | ✅ yes |
|
||||||
|
|
||||||
**Agenda:**
|
**Agenda:**
|
||||||
1. Review the task and requirements
|
1. Review the task and requirements
|
||||||
2. Agree on interfaces and contracts between components
|
2. Agree on interfaces and contracts between components
|
||||||
3. Identify risks and edge cases
|
3. Identify risks and edge cases
|
||||||
4. Assign action items
|
4. Assign action items
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Retrospective
|
## Retrospective
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|-------|-------|
|
|-------|-------|
|
||||||
| **Trigger** | auto |
|
| **Trigger** | auto |
|
||||||
| **When** | after |
|
| **When** | after |
|
||||||
| **Condition** | build failure, test failure, or reviewer rejection |
|
| **Condition** | build failure, test failure, or reviewer rejection |
|
||||||
| **Facilitator** | lead |
|
| **Facilitator** | lead |
|
||||||
| **Participants** | all-involved |
|
| **Participants** | all-involved |
|
||||||
| **Time budget** | focused |
|
| **Time budget** | focused |
|
||||||
| **Enabled** | ✅ yes |
|
| **Enabled** | ✅ yes |
|
||||||
|
|
||||||
**Agenda:**
|
**Agenda:**
|
||||||
1. What happened? (facts only)
|
1. What happened? (facts only)
|
||||||
2. Root cause analysis
|
2. Root cause analysis
|
||||||
3. What should change?
|
3. What should change?
|
||||||
4. Action items for next iteration
|
4. Action items for next iteration
|
||||||
|
|||||||
@@ -1,53 +1,53 @@
|
|||||||
# {Name} — {Role}
|
# {Name} — {Role}
|
||||||
|
|
||||||
> {One-line personality statement — what makes this person tick}
|
> {One-line personality statement — what makes this person tick}
|
||||||
|
|
||||||
## Identity
|
## Identity
|
||||||
|
|
||||||
- **Name:** {Name}
|
- **Name:** {Name}
|
||||||
- **Role:** {Role title}
|
- **Role:** {Role title}
|
||||||
- **Expertise:** {2-3 specific skills relevant to the project}
|
- **Expertise:** {2-3 specific skills relevant to the project}
|
||||||
- **Style:** {How they communicate — direct? thorough? opinionated?}
|
- **Style:** {How they communicate — direct? thorough? opinionated?}
|
||||||
|
|
||||||
## What I Own
|
## What I Own
|
||||||
|
|
||||||
- {Area of responsibility 1}
|
- {Area of responsibility 1}
|
||||||
- {Area of responsibility 2}
|
- {Area of responsibility 2}
|
||||||
- {Area of responsibility 3}
|
- {Area of responsibility 3}
|
||||||
|
|
||||||
## How I Work
|
## How I Work
|
||||||
|
|
||||||
- {Key approach or principle 1}
|
- {Key approach or principle 1}
|
||||||
- {Key approach or principle 2}
|
- {Key approach or principle 2}
|
||||||
- {Pattern or convention I follow}
|
- {Pattern or convention I follow}
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
**I handle:** {types of work this agent does}
|
**I handle:** {types of work this agent does}
|
||||||
|
|
||||||
**I don't handle:** {types of work that belong to other team members}
|
**I don't handle:** {types of work that belong to other team members}
|
||||||
|
|
||||||
**When I'm unsure:** I say so and suggest who might know.
|
**When I'm unsure:** I say so and suggest who might know.
|
||||||
|
|
||||||
**If I review others' work:** On rejection, I may require a different agent to revise (not the original author) or request a new specialist be spawned. The Coordinator enforces this.
|
**If I review others' work:** On rejection, I may require a different agent to revise (not the original author) or request a new specialist be spawned. The Coordinator enforces this.
|
||||||
|
|
||||||
## Model
|
## Model
|
||||||
|
|
||||||
- **Preferred:** auto
|
- **Preferred:** auto
|
||||||
- **Rationale:** Coordinator selects the best model based on task type — cost first unless writing code
|
- **Rationale:** Coordinator selects the best model based on task type — cost first unless writing code
|
||||||
- **Fallback:** Standard chain — the coordinator handles fallback automatically
|
- **Fallback:** Standard chain — the coordinator handles fallback automatically
|
||||||
|
|
||||||
## Collaboration
|
## Collaboration
|
||||||
|
|
||||||
Before starting work, run `git rev-parse --show-toplevel` to find the repo root, or use the `TEAM ROOT` provided in the spawn prompt. All `.squad/` paths must be resolved relative to this root — do not assume CWD is the repo root (you may be in a worktree or subdirectory).
|
Before starting work, run `git rev-parse --show-toplevel` to find the repo root, or use the `TEAM ROOT` provided in the spawn prompt. All `.squad/` paths must be resolved relative to this root — do not assume CWD is the repo root (you may be in a worktree or subdirectory).
|
||||||
|
|
||||||
Before starting work, read `.squad/decisions.md` for team decisions that affect me.
|
Before starting work, read `.squad/decisions.md` for team decisions that affect me.
|
||||||
After making a decision others should know, write it to `.squad/decisions/inbox/{my-name}-{brief-slug}.md` — the Scribe will merge it.
|
After making a decision others should know, write it to `.squad/decisions/inbox/{my-name}-{brief-slug}.md` — the Scribe will merge it.
|
||||||
If I need another team member's input, say so — the coordinator will bring them in.
|
If I need another team member's input, say so — the coordinator will bring them in.
|
||||||
|
|
||||||
## Voice
|
## Voice
|
||||||
|
|
||||||
{1-2 sentences describing personality. Not generic — specific. This agent has OPINIONS.
|
{1-2 sentences describing personality. Not generic — specific. This agent has OPINIONS.
|
||||||
They have preferences. They push back. They have a style that's distinctly theirs.
|
They have preferences. They push back. They have a style that's distinctly theirs.
|
||||||
Example: "Opinionated about test coverage. Will push back if tests are skipped.
|
Example: "Opinionated about test coverage. Will push back if tests are skipped.
|
||||||
Prefers integration tests over mocks. Thinks 80% coverage is the floor, not the ceiling."}
|
Prefers integration tests over mocks. Thinks 80% coverage is the floor, not the ceiling."}
|
||||||
|
|||||||
@@ -1,38 +1,38 @@
|
|||||||
# Constraint Budget Tracking
|
# Constraint Budget Tracking
|
||||||
|
|
||||||
When the user or system imposes constraints (question limits, revision limits, time budgets), maintain a visible counter in your responses and in the artifact.
|
When the user or system imposes constraints (question limits, revision limits, time budgets), maintain a visible counter in your responses and in the artifact.
|
||||||
|
|
||||||
## Format
|
## Format
|
||||||
|
|
||||||
```
|
```
|
||||||
📊 Clarifying questions used: 2 / 3
|
📊 Clarifying questions used: 2 / 3
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- Update the counter each time the constraint is consumed
|
- Update the counter each time the constraint is consumed
|
||||||
- When a constraint is exhausted, state it: `📊 Question budget exhausted (3/3). Proceeding with current information.`
|
- When a constraint is exhausted, state it: `📊 Question budget exhausted (3/3). Proceeding with current information.`
|
||||||
- If no constraints are active, do not display counters
|
- If no constraints are active, do not display counters
|
||||||
- Include the final constraint status in multi-agent artifacts
|
- Include the final constraint status in multi-agent artifacts
|
||||||
|
|
||||||
## Example Session
|
## Example Session
|
||||||
|
|
||||||
```
|
```
|
||||||
Coordinator: Spawning agents to analyze requirements...
|
Coordinator: Spawning agents to analyze requirements...
|
||||||
📊 Clarifying questions used: 0 / 3
|
📊 Clarifying questions used: 0 / 3
|
||||||
|
|
||||||
Agent asks clarification: "Should we support OAuth?"
|
Agent asks clarification: "Should we support OAuth?"
|
||||||
Coordinator: Checking with user...
|
Coordinator: Checking with user...
|
||||||
📊 Clarifying questions used: 1 / 3
|
📊 Clarifying questions used: 1 / 3
|
||||||
|
|
||||||
Agent asks clarification: "What's the rate limit?"
|
Agent asks clarification: "What's the rate limit?"
|
||||||
Coordinator: Checking with user...
|
Coordinator: Checking with user...
|
||||||
📊 Clarifying questions used: 2 / 3
|
📊 Clarifying questions used: 2 / 3
|
||||||
|
|
||||||
Agent asks clarification: "Do we need RBAC?"
|
Agent asks clarification: "Do we need RBAC?"
|
||||||
Coordinator: Checking with user...
|
Coordinator: Checking with user...
|
||||||
📊 Clarifying questions used: 3 / 3
|
📊 Clarifying questions used: 3 / 3
|
||||||
|
|
||||||
Agent asks clarification: "Should we cache responses?"
|
Agent asks clarification: "Should we cache responses?"
|
||||||
Coordinator: 📊 Question budget exhausted (3/3). Proceeding without clarification.
|
Coordinator: 📊 Question budget exhausted (3/3). Proceeding without clarification.
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,229 +1,229 @@
|
|||||||
# Cooperative Rate Limiting for Multi-Agent Deployments
|
# Cooperative Rate Limiting for Multi-Agent Deployments
|
||||||
|
|
||||||
> Coordinate API quota across multiple Ralph instances to prevent cascading failures.
|
> Coordinate API quota across multiple Ralph instances to prevent cascading failures.
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
The [circuit breaker template](ralph-circuit-breaker.md) handles single-instance rate limiting well. But when multiple Ralphs run across machines (or pods on K8s), each instance independently hits API limits:
|
The [circuit breaker template](ralph-circuit-breaker.md) handles single-instance rate limiting well. But when multiple Ralphs run across machines (or pods on K8s), each instance independently hits API limits:
|
||||||
|
|
||||||
- **No coordination** — 5 Ralphs each think they have full API quota
|
- **No coordination** — 5 Ralphs each think they have full API quota
|
||||||
- **Thundering herd** — All Ralphs retry simultaneously after rate limit resets
|
- **Thundering herd** — All Ralphs retry simultaneously after rate limit resets
|
||||||
- **Priority inversion** — Low-priority work exhausts quota before critical work runs
|
- **Priority inversion** — Low-priority work exhausts quota before critical work runs
|
||||||
- **Reactive only** — Circuit opens AFTER 429, wasting the failed request
|
- **Reactive only** — Circuit opens AFTER 429, wasting the failed request
|
||||||
|
|
||||||
## Solution: 6-Pattern Architecture
|
## Solution: 6-Pattern Architecture
|
||||||
|
|
||||||
These patterns layer on top of the existing circuit breaker. Each is independent — adopt one or all.
|
These patterns layer on top of the existing circuit breaker. Each is independent — adopt one or all.
|
||||||
|
|
||||||
### Pattern 1: Traffic Light (RAAS — Rate-Aware Agent Scheduling)
|
### Pattern 1: Traffic Light (RAAS — Rate-Aware Agent Scheduling)
|
||||||
|
|
||||||
Map GitHub API `X-RateLimit-Remaining` to traffic light states:
|
Map GitHub API `X-RateLimit-Remaining` to traffic light states:
|
||||||
|
|
||||||
| State | Remaining % | Behavior |
|
| State | Remaining % | Behavior |
|
||||||
|-------|------------|----------|
|
|-------|------------|----------|
|
||||||
| 🟢 GREEN | >20% | Normal operation |
|
| 🟢 GREEN | >20% | Normal operation |
|
||||||
| 🟡 AMBER | 5–20% | Only P0 agents proceed |
|
| 🟡 AMBER | 5–20% | Only P0 agents proceed |
|
||||||
| 🔴 RED | <5% | Block all except emergency P0 |
|
| 🔴 RED | <5% | Block all except emergency P0 |
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type TrafficLight = 'green' | 'amber' | 'red';
|
type TrafficLight = 'green' | 'amber' | 'red';
|
||||||
|
|
||||||
function getTrafficLight(remaining: number, limit: number): TrafficLight {
|
function getTrafficLight(remaining: number, limit: number): TrafficLight {
|
||||||
const pct = remaining / limit;
|
const pct = remaining / limit;
|
||||||
if (pct > 0.20) return 'green';
|
if (pct > 0.20) return 'green';
|
||||||
if (pct > 0.05) return 'amber';
|
if (pct > 0.05) return 'amber';
|
||||||
return 'red';
|
return 'red';
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldProceed(light: TrafficLight, agentPriority: number): boolean {
|
function shouldProceed(light: TrafficLight, agentPriority: number): boolean {
|
||||||
if (light === 'green') return true;
|
if (light === 'green') return true;
|
||||||
if (light === 'amber') return agentPriority === 0; // P0 only
|
if (light === 'amber') return agentPriority === 0; // P0 only
|
||||||
return false; // RED — block all
|
return false; // RED — block all
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pattern 2: Cooperative Token Pool (CMARP)
|
### Pattern 2: Cooperative Token Pool (CMARP)
|
||||||
|
|
||||||
A shared JSON file (`~/.squad/rate-pool.json`) distributes API quota:
|
A shared JSON file (`~/.squad/rate-pool.json`) distributes API quota:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"totalLimit": 5000,
|
"totalLimit": 5000,
|
||||||
"resetAt": "2026-03-22T20:00:00Z",
|
"resetAt": "2026-03-22T20:00:00Z",
|
||||||
"allocations": {
|
"allocations": {
|
||||||
"picard": { "priority": 0, "allocated": 2000, "used": 450, "leaseExpiry": "2026-03-22T19:55:00Z" },
|
"picard": { "priority": 0, "allocated": 2000, "used": 450, "leaseExpiry": "2026-03-22T19:55:00Z" },
|
||||||
"data": { "priority": 1, "allocated": 1750, "used": 200, "leaseExpiry": "2026-03-22T19:55:00Z" },
|
"data": { "priority": 1, "allocated": 1750, "used": 200, "leaseExpiry": "2026-03-22T19:55:00Z" },
|
||||||
"ralph": { "priority": 2, "allocated": 1250, "used": 100, "leaseExpiry": "2026-03-22T19:55:00Z" }
|
"ralph": { "priority": 2, "allocated": 1250, "used": 100, "leaseExpiry": "2026-03-22T19:55:00Z" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Rules:**
|
**Rules:**
|
||||||
- P0 agents (Lead) get 40% of quota
|
- P0 agents (Lead) get 40% of quota
|
||||||
- P1 agents (specialists) get 35%
|
- P1 agents (specialists) get 35%
|
||||||
- P2 agents (Ralph, Scribe) get 25%
|
- P2 agents (Ralph, Scribe) get 25%
|
||||||
- Stale leases (>5 minutes without heartbeat) are auto-recovered
|
- Stale leases (>5 minutes without heartbeat) are auto-recovered
|
||||||
- Each agent checks their remaining allocation before making API calls
|
- Each agent checks their remaining allocation before making API calls
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface RatePoolAllocation {
|
interface RatePoolAllocation {
|
||||||
priority: number;
|
priority: number;
|
||||||
allocated: number;
|
allocated: number;
|
||||||
used: number;
|
used: number;
|
||||||
leaseExpiry: string;
|
leaseExpiry: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RatePool {
|
interface RatePool {
|
||||||
totalLimit: number;
|
totalLimit: number;
|
||||||
resetAt: string;
|
resetAt: string;
|
||||||
allocations: Record<string, RatePoolAllocation>;
|
allocations: Record<string, RatePoolAllocation>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function canUseQuota(pool: RatePool, agentName: string): boolean {
|
function canUseQuota(pool: RatePool, agentName: string): boolean {
|
||||||
const alloc = pool.allocations[agentName];
|
const alloc = pool.allocations[agentName];
|
||||||
if (!alloc) return true; // Unknown agent — allow (graceful)
|
if (!alloc) return true; // Unknown agent — allow (graceful)
|
||||||
|
|
||||||
// Reclaim stale leases from crashed agents
|
// Reclaim stale leases from crashed agents
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
for (const [name, a] of Object.entries(pool.allocations)) {
|
for (const [name, a] of Object.entries(pool.allocations)) {
|
||||||
if (new Date(a.leaseExpiry) < now && name !== agentName) {
|
if (new Date(a.leaseExpiry) < now && name !== agentName) {
|
||||||
a.allocated = 0; // Reclaim
|
a.allocated = 0; // Reclaim
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return alloc.used < alloc.allocated;
|
return alloc.used < alloc.allocated;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pattern 3: Predictive Circuit Breaker (PCB)
|
### Pattern 3: Predictive Circuit Breaker (PCB)
|
||||||
|
|
||||||
Opens the circuit BEFORE getting a 429 by predicting when quota will run out:
|
Opens the circuit BEFORE getting a 429 by predicting when quota will run out:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface RateSample {
|
interface RateSample {
|
||||||
timestamp: number; // Date.now()
|
timestamp: number; // Date.now()
|
||||||
remaining: number; // from X-RateLimit-Remaining header
|
remaining: number; // from X-RateLimit-Remaining header
|
||||||
}
|
}
|
||||||
|
|
||||||
class PredictiveCircuitBreaker {
|
class PredictiveCircuitBreaker {
|
||||||
private samples: RateSample[] = [];
|
private samples: RateSample[] = [];
|
||||||
private readonly maxSamples = 10;
|
private readonly maxSamples = 10;
|
||||||
private readonly warningThresholdSeconds = 120;
|
private readonly warningThresholdSeconds = 120;
|
||||||
|
|
||||||
addSample(remaining: number): void {
|
addSample(remaining: number): void {
|
||||||
this.samples.push({ timestamp: Date.now(), remaining });
|
this.samples.push({ timestamp: Date.now(), remaining });
|
||||||
if (this.samples.length > this.maxSamples) {
|
if (this.samples.length > this.maxSamples) {
|
||||||
this.samples.shift();
|
this.samples.shift();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Predict seconds until quota exhaustion using linear regression */
|
/** Predict seconds until quota exhaustion using linear regression */
|
||||||
predictExhaustion(): number | null {
|
predictExhaustion(): number | null {
|
||||||
if (this.samples.length < 3) return null;
|
if (this.samples.length < 3) return null;
|
||||||
|
|
||||||
const n = this.samples.length;
|
const n = this.samples.length;
|
||||||
const first = this.samples[0];
|
const first = this.samples[0];
|
||||||
const last = this.samples[n - 1];
|
const last = this.samples[n - 1];
|
||||||
|
|
||||||
const elapsedMs = last.timestamp - first.timestamp;
|
const elapsedMs = last.timestamp - first.timestamp;
|
||||||
if (elapsedMs === 0) return null;
|
if (elapsedMs === 0) return null;
|
||||||
|
|
||||||
const consumedPerMs = (first.remaining - last.remaining) / elapsedMs;
|
const consumedPerMs = (first.remaining - last.remaining) / elapsedMs;
|
||||||
if (consumedPerMs <= 0) return null; // Not consuming — safe
|
if (consumedPerMs <= 0) return null; // Not consuming — safe
|
||||||
|
|
||||||
const msUntilExhausted = last.remaining / consumedPerMs;
|
const msUntilExhausted = last.remaining / consumedPerMs;
|
||||||
return msUntilExhausted / 1000;
|
return msUntilExhausted / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldOpen(): boolean {
|
shouldOpen(): boolean {
|
||||||
const eta = this.predictExhaustion();
|
const eta = this.predictExhaustion();
|
||||||
if (eta === null) return false;
|
if (eta === null) return false;
|
||||||
return eta < this.warningThresholdSeconds;
|
return eta < this.warningThresholdSeconds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pattern 4: Priority Retry Windows (PWJG)
|
### Pattern 4: Priority Retry Windows (PWJG)
|
||||||
|
|
||||||
Non-overlapping jitter windows prevent thundering herd:
|
Non-overlapping jitter windows prevent thundering herd:
|
||||||
|
|
||||||
| Priority | Retry Window | Description |
|
| Priority | Retry Window | Description |
|
||||||
|----------|-------------|-------------|
|
|----------|-------------|-------------|
|
||||||
| P0 (Lead) | 500ms–5s | Recovers first |
|
| P0 (Lead) | 500ms–5s | Recovers first |
|
||||||
| P1 (Specialists) | 2s–30s | Moderate delay |
|
| P1 (Specialists) | 2s–30s | Moderate delay |
|
||||||
| P2 (Ralph/Scribe) | 5s–60s | Most patient |
|
| P2 (Ralph/Scribe) | 5s–60s | Most patient |
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function getRetryDelay(priority: number, attempt: number): number {
|
function getRetryDelay(priority: number, attempt: number): number {
|
||||||
const windows: Record<number, [number, number]> = {
|
const windows: Record<number, [number, number]> = {
|
||||||
0: [500, 5000], // P0: 500ms–5s
|
0: [500, 5000], // P0: 500ms–5s
|
||||||
1: [2000, 30000], // P1: 2s–30s
|
1: [2000, 30000], // P1: 2s–30s
|
||||||
2: [5000, 60000], // P2: 5s–60s
|
2: [5000, 60000], // P2: 5s–60s
|
||||||
};
|
};
|
||||||
|
|
||||||
const [min, max] = windows[priority] ?? windows[2];
|
const [min, max] = windows[priority] ?? windows[2];
|
||||||
const base = Math.min(min * Math.pow(2, attempt), max);
|
const base = Math.min(min * Math.pow(2, attempt), max);
|
||||||
const jitter = Math.random() * base * 0.5;
|
const jitter = Math.random() * base * 0.5;
|
||||||
return base + jitter;
|
return base + jitter;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pattern 5: Resource Epoch Tracker (RET)
|
### Pattern 5: Resource Epoch Tracker (RET)
|
||||||
|
|
||||||
Heartbeat-based lease system for multi-machine deployments:
|
Heartbeat-based lease system for multi-machine deployments:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface ResourceLease {
|
interface ResourceLease {
|
||||||
agent: string;
|
agent: string;
|
||||||
machine: string;
|
machine: string;
|
||||||
leaseStart: string;
|
leaseStart: string;
|
||||||
leaseExpiry: string; // Typically 5 minutes from now
|
leaseExpiry: string; // Typically 5 minutes from now
|
||||||
allocated: number;
|
allocated: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each agent renews its lease every 2 minutes
|
// Each agent renews its lease every 2 minutes
|
||||||
// If lease expires (agent crashed), allocation is reclaimed
|
// If lease expires (agent crashed), allocation is reclaimed
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pattern 6: Cascade Dependency Detector (CDD)
|
### Pattern 6: Cascade Dependency Detector (CDD)
|
||||||
|
|
||||||
Track downstream failures and apply backpressure:
|
Track downstream failures and apply backpressure:
|
||||||
|
|
||||||
```
|
```
|
||||||
Agent A (rate limited) → Agent B (waiting for A) → Agent C (waiting for B)
|
Agent A (rate limited) → Agent B (waiting for A) → Agent C (waiting for B)
|
||||||
↑ Backpressure signal: "don't start new work"
|
↑ Backpressure signal: "don't start new work"
|
||||||
```
|
```
|
||||||
|
|
||||||
When a dependency is rate-limited, upstream agents should pause new work rather than queuing requests that will fail.
|
When a dependency is rate-limited, upstream agents should pause new work rather than queuing requests that will fail.
|
||||||
|
|
||||||
## Kubernetes Integration
|
## Kubernetes Integration
|
||||||
|
|
||||||
On K8s, cooperative rate limiting can use KEDA to scale pods based on API quota:
|
On K8s, cooperative rate limiting can use KEDA to scale pods based on API quota:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: keda.sh/v1alpha1
|
apiVersion: keda.sh/v1alpha1
|
||||||
kind: ScaledObject
|
kind: ScaledObject
|
||||||
spec:
|
spec:
|
||||||
scaleTargetRef:
|
scaleTargetRef:
|
||||||
name: ralph-deployment
|
name: ralph-deployment
|
||||||
triggers:
|
triggers:
|
||||||
- type: external
|
- type: external
|
||||||
metadata:
|
metadata:
|
||||||
scalerAddress: keda-copilot-scaler:6000
|
scalerAddress: keda-copilot-scaler:6000
|
||||||
# Scaler returns 0 when rate limited → pods scale to zero
|
# Scaler returns 0 when rate limited → pods scale to zero
|
||||||
```
|
```
|
||||||
|
|
||||||
See [keda-copilot-scaler](https://github.com/tamirdresher/keda-copilot-scaler) for a complete implementation.
|
See [keda-copilot-scaler](https://github.com/tamirdresher/keda-copilot-scaler) for a complete implementation.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
1. **Minimum viable:** Adopt Pattern 1 (Traffic Light) — read `X-RateLimit-Remaining` from API responses
|
1. **Minimum viable:** Adopt Pattern 1 (Traffic Light) — read `X-RateLimit-Remaining` from API responses
|
||||||
2. **Multi-machine:** Add Pattern 2 (Cooperative Pool) — shared `rate-pool.json`
|
2. **Multi-machine:** Add Pattern 2 (Cooperative Pool) — shared `rate-pool.json`
|
||||||
3. **Production:** Add Pattern 3 (Predictive CB) — prevent 429s entirely
|
3. **Production:** Add Pattern 3 (Predictive CB) — prevent 429s entirely
|
||||||
4. **Kubernetes:** Add KEDA scaler for automatic pod scaling
|
4. **Kubernetes:** Add KEDA scaler for automatic pod scaling
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [Circuit Breaker Template](ralph-circuit-breaker.md) — Foundation patterns
|
- [Circuit Breaker Template](ralph-circuit-breaker.md) — Foundation patterns
|
||||||
- [Squad on AKS](https://github.com/tamirdresher/squad-on-aks) — Production K8s deployment
|
- [Squad on AKS](https://github.com/tamirdresher/squad-on-aks) — Production K8s deployment
|
||||||
- [KEDA Copilot Scaler](https://github.com/tamirdresher/keda-copilot-scaler) — Custom KEDA external scaler
|
- [KEDA Copilot Scaler](https://github.com/tamirdresher/keda-copilot-scaler) — Custom KEDA external scaler
|
||||||
|
|||||||
@@ -1,46 +1,46 @@
|
|||||||
# Copilot Coding Agent — Squad Instructions
|
# Copilot Coding Agent — Squad Instructions
|
||||||
|
|
||||||
You are working on a project that uses **Squad**, an AI team framework. When picking up issues autonomously, follow these guidelines.
|
You are working on a project that uses **Squad**, an AI team framework. When picking up issues autonomously, follow these guidelines.
|
||||||
|
|
||||||
## Team Context
|
## Team Context
|
||||||
|
|
||||||
Before starting work on any issue:
|
Before starting work on any issue:
|
||||||
|
|
||||||
1. Read `.squad/team.md` for the team roster, member roles, and your capability profile.
|
1. Read `.squad/team.md` for the team roster, member roles, and your capability profile.
|
||||||
2. Read `.squad/routing.md` for work routing rules.
|
2. Read `.squad/routing.md` for work routing rules.
|
||||||
3. If the issue has a `squad:{member}` label, read that member's charter at `.squad/agents/{member}/charter.md` to understand their domain expertise and coding style — work in their voice.
|
3. If the issue has a `squad:{member}` label, read that member's charter at `.squad/agents/{member}/charter.md` to understand their domain expertise and coding style — work in their voice.
|
||||||
|
|
||||||
## Capability Self-Check
|
## Capability Self-Check
|
||||||
|
|
||||||
Before starting work, check your capability profile in `.squad/team.md` under the **Coding Agent → Capabilities** section.
|
Before starting work, check your capability profile in `.squad/team.md` under the **Coding Agent → Capabilities** section.
|
||||||
|
|
||||||
- **🟢 Good fit** — proceed autonomously.
|
- **🟢 Good fit** — proceed autonomously.
|
||||||
- **🟡 Needs review** — proceed, but note in the PR description that a squad member should review.
|
- **🟡 Needs review** — proceed, but note in the PR description that a squad member should review.
|
||||||
- **🔴 Not suitable** — do NOT start work. Instead, comment on the issue:
|
- **🔴 Not suitable** — do NOT start work. Instead, comment on the issue:
|
||||||
```
|
```
|
||||||
🤖 This issue doesn't match my capability profile (reason: {why}). Suggesting reassignment to a squad member.
|
🤖 This issue doesn't match my capability profile (reason: {why}). Suggesting reassignment to a squad member.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Branch Naming
|
## Branch Naming
|
||||||
|
|
||||||
Use the squad branch convention:
|
Use the squad branch convention:
|
||||||
```
|
```
|
||||||
squad/{issue-number}-{kebab-case-slug}
|
squad/{issue-number}-{kebab-case-slug}
|
||||||
```
|
```
|
||||||
Example: `squad/42-fix-login-validation`
|
Example: `squad/42-fix-login-validation`
|
||||||
|
|
||||||
## PR Guidelines
|
## PR Guidelines
|
||||||
|
|
||||||
When opening a PR:
|
When opening a PR:
|
||||||
- Reference the issue: `Closes #{issue-number}`
|
- Reference the issue: `Closes #{issue-number}`
|
||||||
- If the issue had a `squad:{member}` label, mention the member: `Working as {member} ({role})`
|
- If the issue had a `squad:{member}` label, mention the member: `Working as {member} ({role})`
|
||||||
- If this is a 🟡 needs-review task, add to the PR description: `⚠️ This task was flagged as "needs review" — please have a squad member review before merging.`
|
- If this is a 🟡 needs-review task, add to the PR description: `⚠️ This task was flagged as "needs review" — please have a squad member review before merging.`
|
||||||
- Follow any project conventions in `.squad/decisions.md`
|
- Follow any project conventions in `.squad/decisions.md`
|
||||||
|
|
||||||
## Decisions
|
## Decisions
|
||||||
|
|
||||||
If you make a decision that affects other team members, write it to:
|
If you make a decision that affects other team members, write it to:
|
||||||
```
|
```
|
||||||
.squad/decisions/inbox/copilot-{brief-slug}.md
|
.squad/decisions/inbox/copilot-{brief-slug}.md
|
||||||
```
|
```
|
||||||
The Scribe will merge it into the shared decisions file.
|
The Scribe will merge it into the shared decisions file.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Project Context
|
# Project Context
|
||||||
|
|
||||||
- **Owner:** {user name}
|
- **Owner:** {user name}
|
||||||
- **Project:** {project description}
|
- **Project:** {project description}
|
||||||
- **Stack:** {languages, frameworks, tools}
|
- **Stack:** {languages, frameworks, tools}
|
||||||
- **Created:** {timestamp}
|
- **Created:** {timestamp}
|
||||||
|
|
||||||
## Learnings
|
## Learnings
|
||||||
|
|
||||||
<!-- Append new learnings below. Each entry is something lasting about the project. -->
|
<!-- Append new learnings below. Each entry is something lasting about the project. -->
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
updated_at: {timestamp}
|
updated_at: {timestamp}
|
||||||
focus_area: {brief description}
|
focus_area: {brief description}
|
||||||
active_issues: []
|
active_issues: []
|
||||||
---
|
---
|
||||||
|
|
||||||
# What We're Focused On
|
# What We're Focused On
|
||||||
|
|
||||||
{Narrative description of current focus — 1-3 sentences. Updated by coordinator at session start.}
|
{Narrative description of current focus — 1-3 sentences. Updated by coordinator at session start.}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
---
|
---
|
||||||
last_updated: {timestamp}
|
last_updated: {timestamp}
|
||||||
---
|
---
|
||||||
|
|
||||||
# Team Wisdom
|
# Team Wisdom
|
||||||
|
|
||||||
Reusable patterns and heuristics learned through work. NOT transcripts — each entry is a distilled, actionable insight.
|
Reusable patterns and heuristics learned through work. NOT transcripts — each entry is a distilled, actionable insight.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
<!-- Append entries below. Format: **Pattern:** description. **Context:** when it applies. -->
|
<!-- Append entries below. Format: **Pattern:** description. **Context:** when it applies. -->
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
<!-- Things we tried that didn't work. **Avoid:** description. **Why:** reason. -->
|
<!-- Things we tried that didn't work. **Avoid:** description. **Why:** reason. -->
|
||||||
|
|||||||
@@ -1,412 +1,412 @@
|
|||||||
# Issue Lifecycle — Repo Connection & PR Flow
|
# Issue Lifecycle — Repo Connection & PR Flow
|
||||||
|
|
||||||
Reference for connecting Squad to a repository and managing the issue→branch→PR→merge lifecycle.
|
Reference for connecting Squad to a repository and managing the issue→branch→PR→merge lifecycle.
|
||||||
|
|
||||||
## Repo Connection Format
|
## Repo Connection Format
|
||||||
|
|
||||||
When connecting Squad to an issue tracker, store the connection in `.squad/team.md`:
|
When connecting Squad to an issue tracker, store the connection in `.squad/team.md`:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
## Issue Source
|
## Issue Source
|
||||||
|
|
||||||
**Repository:** {owner}/{repo}
|
**Repository:** {owner}/{repo}
|
||||||
**Connected:** {date}
|
**Connected:** {date}
|
||||||
**Platform:** {GitHub | Azure DevOps | Planner}
|
**Platform:** {GitHub | Azure DevOps | Planner}
|
||||||
**Filters:**
|
**Filters:**
|
||||||
- Labels: `{label-filter}`
|
- Labels: `{label-filter}`
|
||||||
- Project: `{project-name}` (ADO/Planner only)
|
- Project: `{project-name}` (ADO/Planner only)
|
||||||
- Plan: `{plan-id}` (Planner only)
|
- Plan: `{plan-id}` (Planner only)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Detection triggers:**
|
**Detection triggers:**
|
||||||
- User says "connect to {repo}"
|
- User says "connect to {repo}"
|
||||||
- User says "monitor {repo} for issues"
|
- User says "monitor {repo} for issues"
|
||||||
- Ralph is activated without an issue source
|
- Ralph is activated without an issue source
|
||||||
|
|
||||||
## Platform-Specific Issue States
|
## Platform-Specific Issue States
|
||||||
|
|
||||||
Each platform tracks issue lifecycle differently. Squad normalizes these into a common board state.
|
Each platform tracks issue lifecycle differently. Squad normalizes these into a common board state.
|
||||||
|
|
||||||
### GitHub
|
### GitHub
|
||||||
|
|
||||||
| GitHub State | GitHub API Fields | Squad Board State |
|
| GitHub State | GitHub API Fields | Squad Board State |
|
||||||
|--------------|-------------------|-------------------|
|
|--------------|-------------------|-------------------|
|
||||||
| Open, no assignee | `state: open`, `assignee: null` | `untriaged` |
|
| Open, no assignee | `state: open`, `assignee: null` | `untriaged` |
|
||||||
| Open, assigned, no branch | `state: open`, `assignee: @user`, no linked PR | `assigned` |
|
| Open, assigned, no branch | `state: open`, `assignee: @user`, no linked PR | `assigned` |
|
||||||
| Open, branch exists | `state: open`, linked branch exists | `inProgress` |
|
| Open, branch exists | `state: open`, linked branch exists | `inProgress` |
|
||||||
| Open, PR opened | `state: open`, PR exists, `reviewDecision: null` | `needsReview` |
|
| Open, PR opened | `state: open`, PR exists, `reviewDecision: null` | `needsReview` |
|
||||||
| Open, PR approved | `state: open`, PR `reviewDecision: APPROVED` | `readyToMerge` |
|
| Open, PR approved | `state: open`, PR `reviewDecision: APPROVED` | `readyToMerge` |
|
||||||
| Open, changes requested | `state: open`, PR `reviewDecision: CHANGES_REQUESTED` | `changesRequested` |
|
| Open, changes requested | `state: open`, PR `reviewDecision: CHANGES_REQUESTED` | `changesRequested` |
|
||||||
| Open, CI failure | `state: open`, PR `statusCheckRollup: FAILURE` | `ciFailure` |
|
| Open, CI failure | `state: open`, PR `statusCheckRollup: FAILURE` | `ciFailure` |
|
||||||
| Closed | `state: closed` | `done` |
|
| Closed | `state: closed` | `done` |
|
||||||
|
|
||||||
**Issue labels used by Squad:**
|
**Issue labels used by Squad:**
|
||||||
- `squad` — Issue is in Squad backlog
|
- `squad` — Issue is in Squad backlog
|
||||||
- `squad:{member}` — Assigned to specific agent
|
- `squad:{member}` — Assigned to specific agent
|
||||||
- `squad:untriaged` — Needs triage
|
- `squad:untriaged` — Needs triage
|
||||||
- `go:needs-research` — Needs investigation before implementation
|
- `go:needs-research` — Needs investigation before implementation
|
||||||
- `priority:p{N}` — Priority level (0=critical, 1=high, 2=medium, 3=low)
|
- `priority:p{N}` — Priority level (0=critical, 1=high, 2=medium, 3=low)
|
||||||
- `next-up` — Queued for next agent pickup
|
- `next-up` — Queued for next agent pickup
|
||||||
|
|
||||||
**Branch naming convention:**
|
**Branch naming convention:**
|
||||||
```
|
```
|
||||||
squad/{issue-number}-{kebab-case-slug}
|
squad/{issue-number}-{kebab-case-slug}
|
||||||
```
|
```
|
||||||
Example: `squad/42-fix-login-validation`
|
Example: `squad/42-fix-login-validation`
|
||||||
|
|
||||||
### Azure DevOps
|
### Azure DevOps
|
||||||
|
|
||||||
| ADO State | Squad Board State |
|
| ADO State | Squad Board State |
|
||||||
|-----------|-------------------|
|
|-----------|-------------------|
|
||||||
| New | `untriaged` |
|
| New | `untriaged` |
|
||||||
| Active, no branch | `assigned` |
|
| Active, no branch | `assigned` |
|
||||||
| Active, branch exists | `inProgress` |
|
| Active, branch exists | `inProgress` |
|
||||||
| Active, PR opened | `needsReview` |
|
| Active, PR opened | `needsReview` |
|
||||||
| Active, PR approved | `readyToMerge` |
|
| Active, PR approved | `readyToMerge` |
|
||||||
| Resolved | `done` |
|
| Resolved | `done` |
|
||||||
| Closed | `done` |
|
| Closed | `done` |
|
||||||
|
|
||||||
**Work item tags used by Squad:**
|
**Work item tags used by Squad:**
|
||||||
- `squad` — Work item is in Squad backlog
|
- `squad` — Work item is in Squad backlog
|
||||||
- `squad:{member}` — Assigned to specific agent
|
- `squad:{member}` — Assigned to specific agent
|
||||||
|
|
||||||
**Branch naming convention:**
|
**Branch naming convention:**
|
||||||
```
|
```
|
||||||
squad/{work-item-id}-{kebab-case-slug}
|
squad/{work-item-id}-{kebab-case-slug}
|
||||||
```
|
```
|
||||||
Example: `squad/1234-add-auth-module`
|
Example: `squad/1234-add-auth-module`
|
||||||
|
|
||||||
### Microsoft Planner
|
### Microsoft Planner
|
||||||
|
|
||||||
Planner does not have native Git integration. Squad uses Planner for task tracking and GitHub/ADO for code management.
|
Planner does not have native Git integration. Squad uses Planner for task tracking and GitHub/ADO for code management.
|
||||||
|
|
||||||
| Planner Status | Squad Board State |
|
| Planner Status | Squad Board State |
|
||||||
|----------------|-------------------|
|
|----------------|-------------------|
|
||||||
| Not Started | `untriaged` |
|
| Not Started | `untriaged` |
|
||||||
| In Progress, no PR | `inProgress` |
|
| In Progress, no PR | `inProgress` |
|
||||||
| In Progress, PR opened | `needsReview` |
|
| In Progress, PR opened | `needsReview` |
|
||||||
| Completed | `done` |
|
| Completed | `done` |
|
||||||
|
|
||||||
**Planner→Git workflow:**
|
**Planner→Git workflow:**
|
||||||
1. Task created in Planner bucket
|
1. Task created in Planner bucket
|
||||||
2. Agent reads task from Planner
|
2. Agent reads task from Planner
|
||||||
3. Agent creates branch in GitHub/ADO repo
|
3. Agent creates branch in GitHub/ADO repo
|
||||||
4. Agent opens PR referencing Planner task ID in description
|
4. Agent opens PR referencing Planner task ID in description
|
||||||
5. Agent marks task as "Completed" when PR merges
|
5. Agent marks task as "Completed" when PR merges
|
||||||
|
|
||||||
## Issue → Branch → PR → Merge Lifecycle
|
## Issue → Branch → PR → Merge Lifecycle
|
||||||
|
|
||||||
### 1. Issue Assignment (Triage)
|
### 1. Issue Assignment (Triage)
|
||||||
|
|
||||||
**Trigger:** Ralph detects an untriaged issue or user manually assigns work.
|
**Trigger:** Ralph detects an untriaged issue or user manually assigns work.
|
||||||
|
|
||||||
**Actions:**
|
**Actions:**
|
||||||
1. Read `.squad/routing.md` to determine which agent should handle the issue
|
1. Read `.squad/routing.md` to determine which agent should handle the issue
|
||||||
2. Apply `squad:{member}` label (GitHub) or tag (ADO)
|
2. Apply `squad:{member}` label (GitHub) or tag (ADO)
|
||||||
3. Transition issue to `assigned` state
|
3. Transition issue to `assigned` state
|
||||||
4. Optionally spawn agent immediately if issue is high-priority
|
4. Optionally spawn agent immediately if issue is high-priority
|
||||||
|
|
||||||
**Issue read command:**
|
**Issue read command:**
|
||||||
```bash
|
```bash
|
||||||
# GitHub
|
# GitHub
|
||||||
gh issue view {number} --json number,title,body,labels,assignees
|
gh issue view {number} --json number,title,body,labels,assignees
|
||||||
|
|
||||||
# Azure DevOps
|
# Azure DevOps
|
||||||
az boards work-item show --id {id} --output json
|
az boards work-item show --id {id} --output json
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Branch Creation (Start Work)
|
### 2. Branch Creation (Start Work)
|
||||||
|
|
||||||
**Trigger:** Agent accepts issue assignment and begins work.
|
**Trigger:** Agent accepts issue assignment and begins work.
|
||||||
|
|
||||||
**Actions:**
|
**Actions:**
|
||||||
1. Ensure working on latest base branch (usually `main` or `dev`)
|
1. Ensure working on latest base branch (usually `main` or `dev`)
|
||||||
2. Create feature branch using Squad naming convention
|
2. Create feature branch using Squad naming convention
|
||||||
3. Transition issue to `inProgress` state
|
3. Transition issue to `inProgress` state
|
||||||
|
|
||||||
**Branch creation commands:**
|
**Branch creation commands:**
|
||||||
|
|
||||||
**Standard (single-agent, no parallelism):**
|
**Standard (single-agent, no parallelism):**
|
||||||
```bash
|
```bash
|
||||||
git checkout main && git pull && git checkout -b squad/{issue-number}-{slug}
|
git checkout main && git pull && git checkout -b squad/{issue-number}-{slug}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Worktree (parallel multi-agent):**
|
**Worktree (parallel multi-agent):**
|
||||||
```bash
|
```bash
|
||||||
git worktree add ../worktrees/{issue-number} -b squad/{issue-number}-{slug}
|
git worktree add ../worktrees/{issue-number} -b squad/{issue-number}-{slug}
|
||||||
cd ../worktrees/{issue-number}
|
cd ../worktrees/{issue-number}
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** Worktree support is in progress (#525). Current implementation uses standard checkout.
|
> **Note:** Worktree support is in progress (#525). Current implementation uses standard checkout.
|
||||||
|
|
||||||
### 3. Implementation & Commit
|
### 3. Implementation & Commit
|
||||||
|
|
||||||
**Actions:**
|
**Actions:**
|
||||||
1. Agent makes code changes
|
1. Agent makes code changes
|
||||||
2. Commits reference the issue number
|
2. Commits reference the issue number
|
||||||
3. Pushes branch to remote
|
3. Pushes branch to remote
|
||||||
|
|
||||||
**Commit message format:**
|
**Commit message format:**
|
||||||
```
|
```
|
||||||
{type}({scope}): {description} (#{issue-number})
|
{type}({scope}): {description} (#{issue-number})
|
||||||
|
|
||||||
{detailed explanation if needed}
|
{detailed explanation if needed}
|
||||||
|
|
||||||
{breaking change notice if applicable}
|
{breaking change notice if applicable}
|
||||||
|
|
||||||
Closes #{issue-number}
|
Closes #{issue-number}
|
||||||
|
|
||||||
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Commit types:** `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf`, `style`, `build`, `ci`
|
**Commit types:** `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf`, `style`, `build`, `ci`
|
||||||
|
|
||||||
**Push command:**
|
**Push command:**
|
||||||
```bash
|
```bash
|
||||||
git push -u origin squad/{issue-number}-{slug}
|
git push -u origin squad/{issue-number}-{slug}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. PR Creation
|
### 4. PR Creation
|
||||||
|
|
||||||
**Trigger:** Agent completes implementation and is ready for review.
|
**Trigger:** Agent completes implementation and is ready for review.
|
||||||
|
|
||||||
**Actions:**
|
**Actions:**
|
||||||
1. Open PR from feature branch to base branch
|
1. Open PR from feature branch to base branch
|
||||||
2. Reference issue in PR description
|
2. Reference issue in PR description
|
||||||
3. Apply labels if needed
|
3. Apply labels if needed
|
||||||
4. Transition issue to `needsReview` state
|
4. Transition issue to `needsReview` state
|
||||||
|
|
||||||
**PR creation commands:**
|
**PR creation commands:**
|
||||||
|
|
||||||
**GitHub:**
|
**GitHub:**
|
||||||
```bash
|
```bash
|
||||||
gh pr create --title "{title}" \
|
gh pr create --title "{title}" \
|
||||||
--body "Closes #{issue-number}\n\n{description}" \
|
--body "Closes #{issue-number}\n\n{description}" \
|
||||||
--head squad/{issue-number}-{slug} \
|
--head squad/{issue-number}-{slug} \
|
||||||
--base main
|
--base main
|
||||||
```
|
```
|
||||||
|
|
||||||
**Azure DevOps:**
|
**Azure DevOps:**
|
||||||
```bash
|
```bash
|
||||||
az repos pr create --title "{title}" \
|
az repos pr create --title "{title}" \
|
||||||
--description "Closes #{work-item-id}\n\n{description}" \
|
--description "Closes #{work-item-id}\n\n{description}" \
|
||||||
--source-branch squad/{work-item-id}-{slug} \
|
--source-branch squad/{work-item-id}-{slug} \
|
||||||
--target-branch main
|
--target-branch main
|
||||||
```
|
```
|
||||||
|
|
||||||
**PR description template:**
|
**PR description template:**
|
||||||
```markdown
|
```markdown
|
||||||
Closes #{issue-number}
|
Closes #{issue-number}
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
{what changed}
|
{what changed}
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
- {change 1}
|
- {change 1}
|
||||||
- {change 2}
|
- {change 2}
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
{how this was tested}
|
{how this was tested}
|
||||||
|
|
||||||
{If working as a squad member:}
|
{If working as a squad member:}
|
||||||
Working as {member} ({role})
|
Working as {member} ({role})
|
||||||
|
|
||||||
{If needs human review:}
|
{If needs human review:}
|
||||||
⚠️ This task was flagged as "needs review" — please have a squad member review before merging.
|
⚠️ This task was flagged as "needs review" — please have a squad member review before merging.
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. PR Review & Updates
|
### 5. PR Review & Updates
|
||||||
|
|
||||||
**Review states:**
|
**Review states:**
|
||||||
- **Approved** → `readyToMerge`
|
- **Approved** → `readyToMerge`
|
||||||
- **Changes requested** → `changesRequested`
|
- **Changes requested** → `changesRequested`
|
||||||
- **CI failure** → `ciFailure`
|
- **CI failure** → `ciFailure`
|
||||||
|
|
||||||
**When changes are requested:**
|
**When changes are requested:**
|
||||||
1. Agent addresses feedback
|
1. Agent addresses feedback
|
||||||
2. Commits fixes to the same branch
|
2. Commits fixes to the same branch
|
||||||
3. Pushes updates
|
3. Pushes updates
|
||||||
4. Requests re-review
|
4. Requests re-review
|
||||||
|
|
||||||
**Update workflow:**
|
**Update workflow:**
|
||||||
```bash
|
```bash
|
||||||
# Make changes
|
# Make changes
|
||||||
git add .
|
git add .
|
||||||
git commit -m "fix: address review feedback"
|
git commit -m "fix: address review feedback"
|
||||||
git push
|
git push
|
||||||
```
|
```
|
||||||
|
|
||||||
**Re-request review (GitHub):**
|
**Re-request review (GitHub):**
|
||||||
```bash
|
```bash
|
||||||
gh pr ready {pr-number}
|
gh pr ready {pr-number}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. PR Merge
|
### 6. PR Merge
|
||||||
|
|
||||||
**Trigger:** PR is approved and CI passes.
|
**Trigger:** PR is approved and CI passes.
|
||||||
|
|
||||||
**Merge strategies:**
|
**Merge strategies:**
|
||||||
|
|
||||||
**GitHub (merge commit):**
|
**GitHub (merge commit):**
|
||||||
```bash
|
```bash
|
||||||
gh pr merge {pr-number} --merge --delete-branch
|
gh pr merge {pr-number} --merge --delete-branch
|
||||||
```
|
```
|
||||||
|
|
||||||
**GitHub (squash):**
|
**GitHub (squash):**
|
||||||
```bash
|
```bash
|
||||||
gh pr merge {pr-number} --squash --delete-branch
|
gh pr merge {pr-number} --squash --delete-branch
|
||||||
```
|
```
|
||||||
|
|
||||||
**Azure DevOps:**
|
**Azure DevOps:**
|
||||||
```bash
|
```bash
|
||||||
az repos pr update --id {pr-id} --status completed --delete-source-branch true
|
az repos pr update --id {pr-id} --status completed --delete-source-branch true
|
||||||
```
|
```
|
||||||
|
|
||||||
**Post-merge actions:**
|
**Post-merge actions:**
|
||||||
1. Issue automatically closes (if "Closes #{number}" is in PR description)
|
1. Issue automatically closes (if "Closes #{number}" is in PR description)
|
||||||
2. Feature branch is deleted
|
2. Feature branch is deleted
|
||||||
3. Squad board state transitions to `done`
|
3. Squad board state transitions to `done`
|
||||||
4. Worktree cleanup (if worktree was used — #525)
|
4. Worktree cleanup (if worktree was used — #525)
|
||||||
|
|
||||||
### 7. Cleanup
|
### 7. Cleanup
|
||||||
|
|
||||||
**Standard workflow cleanup:**
|
**Standard workflow cleanup:**
|
||||||
```bash
|
```bash
|
||||||
git checkout main
|
git checkout main
|
||||||
git pull
|
git pull
|
||||||
git branch -d squad/{issue-number}-{slug}
|
git branch -d squad/{issue-number}-{slug}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Worktree cleanup (future, #525):**
|
**Worktree cleanup (future, #525):**
|
||||||
```bash
|
```bash
|
||||||
cd {original-cwd}
|
cd {original-cwd}
|
||||||
git worktree remove ../worktrees/{issue-number}
|
git worktree remove ../worktrees/{issue-number}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Spawn Prompt Additions for Issue Work
|
## Spawn Prompt Additions for Issue Work
|
||||||
|
|
||||||
When spawning an agent to work on an issue, include this context block:
|
When spawning an agent to work on an issue, include this context block:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
## ISSUE CONTEXT
|
## ISSUE CONTEXT
|
||||||
|
|
||||||
**Issue:** #{number} — {title}
|
**Issue:** #{number} — {title}
|
||||||
**Platform:** {GitHub | Azure DevOps | Planner}
|
**Platform:** {GitHub | Azure DevOps | Planner}
|
||||||
**Repository:** {owner}/{repo}
|
**Repository:** {owner}/{repo}
|
||||||
**Assigned to:** {member}
|
**Assigned to:** {member}
|
||||||
|
|
||||||
**Description:**
|
**Description:**
|
||||||
{issue body}
|
{issue body}
|
||||||
|
|
||||||
**Labels/Tags:**
|
**Labels/Tags:**
|
||||||
{labels}
|
{labels}
|
||||||
|
|
||||||
**Acceptance Criteria:**
|
**Acceptance Criteria:**
|
||||||
{criteria if present in issue}
|
{criteria if present in issue}
|
||||||
|
|
||||||
**Branch:** `squad/{issue-number}-{slug}`
|
**Branch:** `squad/{issue-number}-{slug}`
|
||||||
|
|
||||||
**Your task:**
|
**Your task:**
|
||||||
{specific directive to the agent}
|
{specific directive to the agent}
|
||||||
|
|
||||||
**After completing work:**
|
**After completing work:**
|
||||||
1. Commit with message referencing issue number
|
1. Commit with message referencing issue number
|
||||||
2. Push branch
|
2. Push branch
|
||||||
3. Open PR using:
|
3. Open PR using:
|
||||||
```
|
```
|
||||||
gh pr create --title "{title}" --body "Closes #{number}\n\n{description}" --head squad/{issue-number}-{slug} --base {base-branch}
|
gh pr create --title "{title}" --body "Closes #{number}\n\n{description}" --head squad/{issue-number}-{slug} --base {base-branch}
|
||||||
```
|
```
|
||||||
4. Report PR URL to coordinator
|
4. Report PR URL to coordinator
|
||||||
```
|
```
|
||||||
|
|
||||||
## Ralph's Role in Issue Lifecycle
|
## Ralph's Role in Issue Lifecycle
|
||||||
|
|
||||||
Ralph (the work monitor) continuously checks issue and PR state:
|
Ralph (the work monitor) continuously checks issue and PR state:
|
||||||
|
|
||||||
1. **Triage:** Detects untriaged issues, assigns `squad:{member}` labels
|
1. **Triage:** Detects untriaged issues, assigns `squad:{member}` labels
|
||||||
2. **Spawn:** Launches agents for assigned issues
|
2. **Spawn:** Launches agents for assigned issues
|
||||||
3. **Monitor:** Tracks PR state transitions (needsReview → changesRequested → readyToMerge)
|
3. **Monitor:** Tracks PR state transitions (needsReview → changesRequested → readyToMerge)
|
||||||
4. **Merge:** Automatically merges approved PRs
|
4. **Merge:** Automatically merges approved PRs
|
||||||
5. **Cleanup:** Marks issues as done when PRs merge
|
5. **Cleanup:** Marks issues as done when PRs merge
|
||||||
|
|
||||||
**Ralph's work-check cycle:**
|
**Ralph's work-check cycle:**
|
||||||
```
|
```
|
||||||
Scan → Categorize → Dispatch → Watch → Report → Loop
|
Scan → Categorize → Dispatch → Watch → Report → Loop
|
||||||
```
|
```
|
||||||
|
|
||||||
See `.squad/templates/ralph-reference.md` for Ralph's full lifecycle.
|
See `.squad/templates/ralph-reference.md` for Ralph's full lifecycle.
|
||||||
|
|
||||||
## PR Review Handling
|
## PR Review Handling
|
||||||
|
|
||||||
### Automated Approval (CI-only projects)
|
### Automated Approval (CI-only projects)
|
||||||
|
|
||||||
If the project has no human reviewers configured:
|
If the project has no human reviewers configured:
|
||||||
1. PR opens
|
1. PR opens
|
||||||
2. CI runs
|
2. CI runs
|
||||||
3. If CI passes, Ralph auto-merges
|
3. If CI passes, Ralph auto-merges
|
||||||
4. Issue closes
|
4. Issue closes
|
||||||
|
|
||||||
### Human Review Required
|
### Human Review Required
|
||||||
|
|
||||||
If the project requires human approval:
|
If the project requires human approval:
|
||||||
1. PR opens
|
1. PR opens
|
||||||
2. Human reviewer is notified (GitHub/ADO notifications)
|
2. Human reviewer is notified (GitHub/ADO notifications)
|
||||||
3. Reviewer approves or requests changes
|
3. Reviewer approves or requests changes
|
||||||
4. If approved + CI passes, Ralph merges
|
4. If approved + CI passes, Ralph merges
|
||||||
5. If changes requested, agent addresses feedback
|
5. If changes requested, agent addresses feedback
|
||||||
|
|
||||||
### Squad Member Review
|
### Squad Member Review
|
||||||
|
|
||||||
If the issue was assigned to a squad member and they authored the PR:
|
If the issue was assigned to a squad member and they authored the PR:
|
||||||
1. Another squad member reviews (conflict of interest avoidance)
|
1. Another squad member reviews (conflict of interest avoidance)
|
||||||
2. Original author is locked out from re-working rejected code (rejection lockout)
|
2. Original author is locked out from re-working rejected code (rejection lockout)
|
||||||
3. Reviewer can approve edits or reject outright
|
3. Reviewer can approve edits or reject outright
|
||||||
|
|
||||||
## Common Issue Lifecycle Patterns
|
## Common Issue Lifecycle Patterns
|
||||||
|
|
||||||
### Pattern 1: Quick Fix (Single Agent, No Review)
|
### Pattern 1: Quick Fix (Single Agent, No Review)
|
||||||
```
|
```
|
||||||
Issue created → Assigned to agent → Branch created → Code fixed →
|
Issue created → Assigned to agent → Branch created → Code fixed →
|
||||||
PR opened → CI passes → Auto-merged → Issue closed
|
PR opened → CI passes → Auto-merged → Issue closed
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pattern 2: Feature Development (Human Review)
|
### Pattern 2: Feature Development (Human Review)
|
||||||
```
|
```
|
||||||
Issue created → Assigned to agent → Branch created → Feature implemented →
|
Issue created → Assigned to agent → Branch created → Feature implemented →
|
||||||
PR opened → Human reviews → Changes requested → Agent fixes →
|
PR opened → Human reviews → Changes requested → Agent fixes →
|
||||||
Re-reviewed → Approved → Merged → Issue closed
|
Re-reviewed → Approved → Merged → Issue closed
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pattern 3: Research-Then-Implement
|
### Pattern 3: Research-Then-Implement
|
||||||
```
|
```
|
||||||
Issue created → Labeled `go:needs-research` → Research agent spawned →
|
Issue created → Labeled `go:needs-research` → Research agent spawned →
|
||||||
Research documented → Research PR merged → Implementation issue created →
|
Research documented → Research PR merged → Implementation issue created →
|
||||||
Implementation agent spawned → Feature built → PR merged
|
Implementation agent spawned → Feature built → PR merged
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pattern 4: Parallel Multi-Agent (Future, #525)
|
### Pattern 4: Parallel Multi-Agent (Future, #525)
|
||||||
```
|
```
|
||||||
Epic issue created → Decomposed into sub-issues → Each sub-issue assigned →
|
Epic issue created → Decomposed into sub-issues → Each sub-issue assigned →
|
||||||
Multiple agents work in parallel worktrees → PRs opened concurrently →
|
Multiple agents work in parallel worktrees → PRs opened concurrently →
|
||||||
All PRs reviewed → All PRs merged → Epic closed
|
All PRs reviewed → All PRs merged → Epic closed
|
||||||
```
|
```
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- ❌ Creating branches without linking to an issue
|
- ❌ Creating branches without linking to an issue
|
||||||
- ❌ Committing without issue reference in message
|
- ❌ Committing without issue reference in message
|
||||||
- ❌ Opening PRs without "Closes #{number}" in description
|
- ❌ Opening PRs without "Closes #{number}" in description
|
||||||
- ❌ Merging PRs before CI passes
|
- ❌ Merging PRs before CI passes
|
||||||
- ❌ Leaving feature branches undeleted after merge
|
- ❌ Leaving feature branches undeleted after merge
|
||||||
- ❌ Using `checkout -b` when parallel agents are active (causes working directory conflicts)
|
- ❌ Using `checkout -b` when parallel agents are active (causes working directory conflicts)
|
||||||
- ❌ Manually transitioning issue states — let the platform and Squad automation handle it
|
- ❌ Manually transitioning issue states — let the platform and Squad automation handle it
|
||||||
- ❌ Skipping the branch naming convention — breaks Ralph's tracking logic
|
- ❌ Skipping the branch naming convention — breaks Ralph's tracking logic
|
||||||
|
|
||||||
## Migration Notes
|
## Migration Notes
|
||||||
|
|
||||||
**v0.8.x → v0.9.x (Worktree Support):**
|
**v0.8.x → v0.9.x (Worktree Support):**
|
||||||
- `checkout -b` → `git worktree add` for parallel agents
|
- `checkout -b` → `git worktree add` for parallel agents
|
||||||
- Worktree cleanup added to post-merge flow
|
- Worktree cleanup added to post-merge flow
|
||||||
- `TEAM_ROOT` passing to agents to support worktree-aware state resolution
|
- `TEAM_ROOT` passing to agents to support worktree-aware state resolution
|
||||||
|
|
||||||
This template will be updated as worktree lifecycle support lands in #525.
|
This template will be updated as worktree lifecycle support lands in #525.
|
||||||
|
|||||||
@@ -1,164 +1,164 @@
|
|||||||
# KEDA External Scaler for GitHub Issue-Driven Agent Autoscaling
|
# KEDA External Scaler for GitHub Issue-Driven Agent Autoscaling
|
||||||
|
|
||||||
> Scale agent pods to zero when idle, up when work arrives — driven by GitHub Issues.
|
> Scale agent pods to zero when idle, up when work arrives — driven by GitHub Issues.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
When running Squad on Kubernetes, agent pods sit idle when no work exists. [KEDA](https://keda.sh) (Kubernetes Event-Driven Autoscaler) solves this for queue-based workloads, but GitHub Issues isn't a native KEDA trigger.
|
When running Squad on Kubernetes, agent pods sit idle when no work exists. [KEDA](https://keda.sh) (Kubernetes Event-Driven Autoscaler) solves this for queue-based workloads, but GitHub Issues isn't a native KEDA trigger.
|
||||||
|
|
||||||
The `keda-copilot-scaler` is a KEDA External Scaler (gRPC) that bridges this gap:
|
The `keda-copilot-scaler` is a KEDA External Scaler (gRPC) that bridges this gap:
|
||||||
1. Polls GitHub API for issues matching specific labels (e.g., `squad:copilot`)
|
1. Polls GitHub API for issues matching specific labels (e.g., `squad:copilot`)
|
||||||
2. Reports queue depth as a KEDA metric
|
2. Reports queue depth as a KEDA metric
|
||||||
3. Handles rate limits gracefully (Retry-After, exponential backoff)
|
3. Handles rate limits gracefully (Retry-After, exponential backoff)
|
||||||
4. Supports composite scaling decisions
|
4. Supports composite scaling decisions
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- Kubernetes cluster with KEDA v2.x installed
|
- Kubernetes cluster with KEDA v2.x installed
|
||||||
- GitHub personal access token (PAT) with `repo` scope
|
- GitHub personal access token (PAT) with `repo` scope
|
||||||
- Helm 3.x
|
- Helm 3.x
|
||||||
|
|
||||||
### 1. Install the Scaler
|
### 1. Install the Scaler
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
helm install keda-copilot-scaler oci://ghcr.io/tamirdresher/keda-copilot-scaler \
|
helm install keda-copilot-scaler oci://ghcr.io/tamirdresher/keda-copilot-scaler \
|
||||||
--namespace squad-scaler --create-namespace \
|
--namespace squad-scaler --create-namespace \
|
||||||
--set github.owner=YOUR_ORG \
|
--set github.owner=YOUR_ORG \
|
||||||
--set github.repo=YOUR_REPO \
|
--set github.repo=YOUR_REPO \
|
||||||
--set github.token=YOUR_TOKEN
|
--set github.token=YOUR_TOKEN
|
||||||
```
|
```
|
||||||
|
|
||||||
Or with Kustomize:
|
Or with Kustomize:
|
||||||
```bash
|
```bash
|
||||||
kubectl apply -k https://github.com/tamirdresher/keda-copilot-scaler/deploy/kustomize
|
kubectl apply -k https://github.com/tamirdresher/keda-copilot-scaler/deploy/kustomize
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Create a ScaledObject
|
### 2. Create a ScaledObject
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: keda.sh/v1alpha1
|
apiVersion: keda.sh/v1alpha1
|
||||||
kind: ScaledObject
|
kind: ScaledObject
|
||||||
metadata:
|
metadata:
|
||||||
name: picard-scaler
|
name: picard-scaler
|
||||||
namespace: squad
|
namespace: squad
|
||||||
spec:
|
spec:
|
||||||
scaleTargetRef:
|
scaleTargetRef:
|
||||||
name: picard-deployment
|
name: picard-deployment
|
||||||
minReplicaCount: 0 # Scale to zero when idle
|
minReplicaCount: 0 # Scale to zero when idle
|
||||||
maxReplicaCount: 3
|
maxReplicaCount: 3
|
||||||
pollingInterval: 30 # Check every 30 seconds
|
pollingInterval: 30 # Check every 30 seconds
|
||||||
cooldownPeriod: 300 # Wait 5 minutes before scaling down
|
cooldownPeriod: 300 # Wait 5 minutes before scaling down
|
||||||
triggers:
|
triggers:
|
||||||
- type: external
|
- type: external
|
||||||
metadata:
|
metadata:
|
||||||
scalerAddress: keda-copilot-scaler.squad-scaler.svc.cluster.local:6000
|
scalerAddress: keda-copilot-scaler.squad-scaler.svc.cluster.local:6000
|
||||||
owner: your-org
|
owner: your-org
|
||||||
repo: your-repo
|
repo: your-repo
|
||||||
labels: squad:copilot # Only count issues with this label
|
labels: squad:copilot # Only count issues with this label
|
||||||
threshold: "1" # Scale up when >= 1 issue exists
|
threshold: "1" # Scale up when >= 1 issue exists
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Verify
|
### 3. Verify
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check the scaler is running
|
# Check the scaler is running
|
||||||
kubectl get pods -n squad-scaler
|
kubectl get pods -n squad-scaler
|
||||||
|
|
||||||
# Check ScaledObject status
|
# Check ScaledObject status
|
||||||
kubectl get scaledobject picard-scaler -n squad
|
kubectl get scaledobject picard-scaler -n squad
|
||||||
|
|
||||||
# Watch scaling events
|
# Watch scaling events
|
||||||
kubectl get events -n squad --watch
|
kubectl get events -n squad --watch
|
||||||
```
|
```
|
||||||
|
|
||||||
## Scaling Behavior
|
## Scaling Behavior
|
||||||
|
|
||||||
| Open Issues | Target Replicas | Behavior |
|
| Open Issues | Target Replicas | Behavior |
|
||||||
|------------|----------------|----------|
|
|------------|----------------|----------|
|
||||||
| 0 | 0 | Scale to zero — save resources |
|
| 0 | 0 | Scale to zero — save resources |
|
||||||
| 1–3 | 1 | Single agent handles work |
|
| 1–3 | 1 | Single agent handles work |
|
||||||
| 4–10 | 2 | Scale up for parallel processing |
|
| 4–10 | 2 | Scale up for parallel processing |
|
||||||
| 10+ | 3 (max) | Maximum parallelism |
|
| 10+ | 3 (max) | Maximum parallelism |
|
||||||
|
|
||||||
The threshold and max replicas are configurable per ScaledObject.
|
The threshold and max replicas are configurable per ScaledObject.
|
||||||
|
|
||||||
## Rate Limit Awareness
|
## Rate Limit Awareness
|
||||||
|
|
||||||
The scaler tracks GitHub API rate limits:
|
The scaler tracks GitHub API rate limits:
|
||||||
- Reads `X-RateLimit-Remaining` from API responses
|
- Reads `X-RateLimit-Remaining` from API responses
|
||||||
- Backs off when quota is low (< 100 remaining)
|
- Backs off when quota is low (< 100 remaining)
|
||||||
- Reports rate limit metrics as secondary KEDA triggers
|
- Reports rate limit metrics as secondary KEDA triggers
|
||||||
- Never exhausts API quota from polling
|
- Never exhausts API quota from polling
|
||||||
|
|
||||||
## Integration with Squad
|
## Integration with Squad
|
||||||
|
|
||||||
### Machine Capabilities (#514)
|
### Machine Capabilities (#514)
|
||||||
|
|
||||||
Combine with machine capability labels for intelligent scheduling:
|
Combine with machine capability labels for intelligent scheduling:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# Only scale pods on GPU-capable nodes
|
# Only scale pods on GPU-capable nodes
|
||||||
spec:
|
spec:
|
||||||
template:
|
template:
|
||||||
spec:
|
spec:
|
||||||
nodeSelector:
|
nodeSelector:
|
||||||
node.squad.dev/gpu: "true"
|
node.squad.dev/gpu: "true"
|
||||||
triggers:
|
triggers:
|
||||||
- type: external
|
- type: external
|
||||||
metadata:
|
metadata:
|
||||||
labels: squad:copilot,needs:gpu
|
labels: squad:copilot,needs:gpu
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cooperative Rate Limiting (#515)
|
### Cooperative Rate Limiting (#515)
|
||||||
|
|
||||||
The scaler exposes rate limit metrics that feed into the cooperative rate limiting system:
|
The scaler exposes rate limit metrics that feed into the cooperative rate limiting system:
|
||||||
- Current `X-RateLimit-Remaining` value
|
- Current `X-RateLimit-Remaining` value
|
||||||
- Predicted time to exhaustion (from predictive circuit breaker)
|
- Predicted time to exhaustion (from predictive circuit breaker)
|
||||||
- Can return 0 target replicas when rate limited → pods scale to zero
|
- Can return 0 target replicas when rate limited → pods scale to zero
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
GitHub API KEDA Kubernetes
|
GitHub API KEDA Kubernetes
|
||||||
┌──────────┐ ┌──────────┐ ┌──────────────┐
|
┌──────────┐ ┌──────────┐ ┌──────────────┐
|
||||||
│ Issues │◄── poll ──►│ Scaler │──metrics─►│ HPA / KEDA │
|
│ Issues │◄── poll ──►│ Scaler │──metrics─►│ HPA / KEDA │
|
||||||
│ (REST) │ │ (gRPC) │ │ Controller │
|
│ (REST) │ │ (gRPC) │ │ Controller │
|
||||||
└──────────┘ └──────────┘ └──────┬───────┘
|
└──────────┘ └──────────┘ └──────┬───────┘
|
||||||
│
|
│
|
||||||
scale up/down
|
scale up/down
|
||||||
│
|
│
|
||||||
┌──────▼───────┐
|
┌──────▼───────┐
|
||||||
│ Agent Pods │
|
│ Agent Pods │
|
||||||
│ (0–N replicas)│
|
│ (0–N replicas)│
|
||||||
└──────────────┘
|
└──────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration Reference
|
## Configuration Reference
|
||||||
|
|
||||||
| Parameter | Default | Description |
|
| Parameter | Default | Description |
|
||||||
|-----------|---------|-------------|
|
|-----------|---------|-------------|
|
||||||
| `github.owner` | — | Repository owner |
|
| `github.owner` | — | Repository owner |
|
||||||
| `github.repo` | — | Repository name |
|
| `github.repo` | — | Repository name |
|
||||||
| `github.token` | — | GitHub PAT with `repo` scope |
|
| `github.token` | — | GitHub PAT with `repo` scope |
|
||||||
| `github.labels` | `squad:copilot` | Comma-separated label filter |
|
| `github.labels` | `squad:copilot` | Comma-separated label filter |
|
||||||
| `scaler.port` | `6000` | gRPC server port |
|
| `scaler.port` | `6000` | gRPC server port |
|
||||||
| `scaler.pollInterval` | `30s` | GitHub API polling interval |
|
| `scaler.pollInterval` | `30s` | GitHub API polling interval |
|
||||||
| `scaler.rateLimitThreshold` | `100` | Stop polling below this remaining |
|
| `scaler.rateLimitThreshold` | `100` | Stop polling below this remaining |
|
||||||
|
|
||||||
## Source & Contributing
|
## Source & Contributing
|
||||||
|
|
||||||
- **Repository:** [tamirdresher/keda-copilot-scaler](https://github.com/tamirdresher/keda-copilot-scaler)
|
- **Repository:** [tamirdresher/keda-copilot-scaler](https://github.com/tamirdresher/keda-copilot-scaler)
|
||||||
- **License:** MIT
|
- **License:** MIT
|
||||||
- **Language:** Go
|
- **Language:** Go
|
||||||
- **Tests:** 51 passing (unit + integration)
|
- **Tests:** 51 passing (unit + integration)
|
||||||
- **CI:** GitHub Actions
|
- **CI:** GitHub Actions
|
||||||
|
|
||||||
The scaler is maintained as a standalone project. PRs and issues welcome.
|
The scaler is maintained as a standalone project. PRs and issues welcome.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [KEDA External Scalers](https://keda.sh/docs/latest/concepts/external-scalers/) — KEDA documentation
|
- [KEDA External Scalers](https://keda.sh/docs/latest/concepts/external-scalers/) — KEDA documentation
|
||||||
- [Squad on AKS](https://github.com/tamirdresher/squad-on-aks) — Full Kubernetes deployment example
|
- [Squad on AKS](https://github.com/tamirdresher/squad-on-aks) — Full Kubernetes deployment example
|
||||||
- [Machine Capabilities](machine-capabilities.md) — Capability-based routing (#514)
|
- [Machine Capabilities](machine-capabilities.md) — Capability-based routing (#514)
|
||||||
- [Cooperative Rate Limiting](cooperative-rate-limiting.md) — Multi-agent rate management (#515)
|
- [Cooperative Rate Limiting](cooperative-rate-limiting.md) — Multi-agent rate management (#515)
|
||||||
|
|||||||
@@ -1,75 +1,75 @@
|
|||||||
# Machine Capability Discovery & Label-Based Routing
|
# Machine Capability Discovery & Label-Based Routing
|
||||||
|
|
||||||
> Enable Ralph to skip issues requiring capabilities the current machine lacks.
|
> Enable Ralph to skip issues requiring capabilities the current machine lacks.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
When running Squad across multiple machines (laptops, DevBoxes, GPU servers, Kubernetes nodes), each machine has different tooling. The capability system lets you declare what each machine can do, and Ralph automatically routes work accordingly.
|
When running Squad across multiple machines (laptops, DevBoxes, GPU servers, Kubernetes nodes), each machine has different tooling. The capability system lets you declare what each machine can do, and Ralph automatically routes work accordingly.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
### 1. Create a Capabilities Manifest
|
### 1. Create a Capabilities Manifest
|
||||||
|
|
||||||
Create `~/.squad/machine-capabilities.json` (user-wide) or `.squad/machine-capabilities.json` (project-local):
|
Create `~/.squad/machine-capabilities.json` (user-wide) or `.squad/machine-capabilities.json` (project-local):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"machine": "MY-LAPTOP",
|
"machine": "MY-LAPTOP",
|
||||||
"capabilities": ["browser", "personal-gh", "onedrive"],
|
"capabilities": ["browser", "personal-gh", "onedrive"],
|
||||||
"missing": ["gpu", "docker", "azure-speech"],
|
"missing": ["gpu", "docker", "azure-speech"],
|
||||||
"lastUpdated": "2026-03-22T00:00:00Z"
|
"lastUpdated": "2026-03-22T00:00:00Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Label Issues with Requirements
|
### 2. Label Issues with Requirements
|
||||||
|
|
||||||
Add `needs:*` labels to issues that require specific capabilities:
|
Add `needs:*` labels to issues that require specific capabilities:
|
||||||
|
|
||||||
| Label | Meaning |
|
| Label | Meaning |
|
||||||
|-------|---------|
|
|-------|---------|
|
||||||
| `needs:browser` | Requires Playwright / browser automation |
|
| `needs:browser` | Requires Playwright / browser automation |
|
||||||
| `needs:gpu` | Requires NVIDIA GPU |
|
| `needs:gpu` | Requires NVIDIA GPU |
|
||||||
| `needs:personal-gh` | Requires personal GitHub account |
|
| `needs:personal-gh` | Requires personal GitHub account |
|
||||||
| `needs:emu-gh` | Requires Enterprise Managed User account |
|
| `needs:emu-gh` | Requires Enterprise Managed User account |
|
||||||
| `needs:azure-cli` | Requires authenticated Azure CLI |
|
| `needs:azure-cli` | Requires authenticated Azure CLI |
|
||||||
| `needs:docker` | Requires Docker daemon |
|
| `needs:docker` | Requires Docker daemon |
|
||||||
| `needs:onedrive` | Requires OneDrive sync |
|
| `needs:onedrive` | Requires OneDrive sync |
|
||||||
| `needs:teams-mcp` | Requires Teams MCP tools |
|
| `needs:teams-mcp` | Requires Teams MCP tools |
|
||||||
|
|
||||||
Custom capabilities are supported — any `needs:X` label works if `X` is in the machine's `capabilities` array.
|
Custom capabilities are supported — any `needs:X` label works if `X` is in the machine's `capabilities` array.
|
||||||
|
|
||||||
### 3. Run Ralph
|
### 3. Run Ralph
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
squad watch --interval 5
|
squad watch --interval 5
|
||||||
```
|
```
|
||||||
|
|
||||||
Ralph will log skipped issues:
|
Ralph will log skipped issues:
|
||||||
```
|
```
|
||||||
⏭️ Skipping #42 "Train ML model" — missing: gpu
|
⏭️ Skipping #42 "Train ML model" — missing: gpu
|
||||||
✓ Triaged #43 "Fix CSS layout" → Picard (routing-rule)
|
✓ Triaged #43 "Fix CSS layout" → Picard (routing-rule)
|
||||||
```
|
```
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
1. Ralph loads `machine-capabilities.json` at startup
|
1. Ralph loads `machine-capabilities.json` at startup
|
||||||
2. For each open issue, Ralph extracts `needs:*` labels
|
2. For each open issue, Ralph extracts `needs:*` labels
|
||||||
3. If any required capability is missing, the issue is skipped
|
3. If any required capability is missing, the issue is skipped
|
||||||
4. Issues without `needs:*` labels are always processed (opt-in system)
|
4. Issues without `needs:*` labels are always processed (opt-in system)
|
||||||
|
|
||||||
## Kubernetes Integration
|
## Kubernetes Integration
|
||||||
|
|
||||||
On Kubernetes, machine capabilities map to node labels:
|
On Kubernetes, machine capabilities map to node labels:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# Node labels (set by capability DaemonSet or manually)
|
# Node labels (set by capability DaemonSet or manually)
|
||||||
node.squad.dev/gpu: "true"
|
node.squad.dev/gpu: "true"
|
||||||
node.squad.dev/browser: "true"
|
node.squad.dev/browser: "true"
|
||||||
|
|
||||||
# Pod spec uses nodeSelector
|
# Pod spec uses nodeSelector
|
||||||
spec:
|
spec:
|
||||||
nodeSelector:
|
nodeSelector:
|
||||||
node.squad.dev/gpu: "true"
|
node.squad.dev/gpu: "true"
|
||||||
```
|
```
|
||||||
|
|
||||||
A DaemonSet can run capability discovery on each node and maintain labels automatically. See the [squad-on-aks](https://github.com/tamirdresher/squad-on-aks) project for a complete Kubernetes deployment example.
|
A DaemonSet can run capability discovery on each node and maintain labels automatically. See the [squad-on-aks](https://github.com/tamirdresher/squad-on-aks) project for a complete Kubernetes deployment example.
|
||||||
@@ -1,90 +1,90 @@
|
|||||||
# MCP Integration — Configuration and Samples
|
# MCP Integration — Configuration and Samples
|
||||||
|
|
||||||
MCP (Model Context Protocol) servers extend Squad with tools for external services — Trello, Aspire dashboards, Azure, Notion, and more. The user configures MCP servers in their environment; Squad discovers and uses them.
|
MCP (Model Context Protocol) servers extend Squad with tools for external services — Trello, Aspire dashboards, Azure, Notion, and more. The user configures MCP servers in their environment; Squad discovers and uses them.
|
||||||
|
|
||||||
> **Full patterns:** Read `.squad/skills/mcp-tool-discovery/SKILL.md` for discovery patterns, domain-specific usage, and graceful degradation.
|
> **Full patterns:** Read `.squad/skills/mcp-tool-discovery/SKILL.md` for discovery patterns, domain-specific usage, and graceful degradation.
|
||||||
|
|
||||||
## Config File Locations
|
## Config File Locations
|
||||||
|
|
||||||
Users configure MCP servers at these locations (checked in priority order):
|
Users configure MCP servers at these locations (checked in priority order):
|
||||||
1. **Repository-level:** `.copilot/mcp-config.json` (team-shared, committed to repo)
|
1. **Repository-level:** `.copilot/mcp-config.json` (team-shared, committed to repo)
|
||||||
2. **Workspace-level:** `.vscode/mcp.json` (VS Code workspaces)
|
2. **Workspace-level:** `.vscode/mcp.json` (VS Code workspaces)
|
||||||
3. **User-level:** `~/.copilot/mcp-config.json` (personal)
|
3. **User-level:** `~/.copilot/mcp-config.json` (personal)
|
||||||
4. **CLI override:** `--additional-mcp-config` flag (session-specific)
|
4. **CLI override:** `--additional-mcp-config` flag (session-specific)
|
||||||
|
|
||||||
## Sample Config — Trello
|
## Sample Config — Trello
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"trello": {
|
"trello": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "@trello/mcp-server"],
|
"args": ["-y", "@trello/mcp-server"],
|
||||||
"env": {
|
"env": {
|
||||||
"TRELLO_API_KEY": "${TRELLO_API_KEY}",
|
"TRELLO_API_KEY": "${TRELLO_API_KEY}",
|
||||||
"TRELLO_TOKEN": "${TRELLO_TOKEN}"
|
"TRELLO_TOKEN": "${TRELLO_TOKEN}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Sample Config — GitHub
|
## Sample Config — GitHub
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"github": {
|
"github": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||||
"env": {
|
"env": {
|
||||||
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
|
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Sample Config — Azure
|
## Sample Config — Azure
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"azure": {
|
"azure": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "@azure/mcp-server"],
|
"args": ["-y", "@azure/mcp-server"],
|
||||||
"env": {
|
"env": {
|
||||||
"AZURE_SUBSCRIPTION_ID": "${AZURE_SUBSCRIPTION_ID}",
|
"AZURE_SUBSCRIPTION_ID": "${AZURE_SUBSCRIPTION_ID}",
|
||||||
"AZURE_CLIENT_ID": "${AZURE_CLIENT_ID}",
|
"AZURE_CLIENT_ID": "${AZURE_CLIENT_ID}",
|
||||||
"AZURE_CLIENT_SECRET": "${AZURE_CLIENT_SECRET}",
|
"AZURE_CLIENT_SECRET": "${AZURE_CLIENT_SECRET}",
|
||||||
"AZURE_TENANT_ID": "${AZURE_TENANT_ID}"
|
"AZURE_TENANT_ID": "${AZURE_TENANT_ID}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Sample Config — Aspire
|
## Sample Config — Aspire
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"aspire": {
|
"aspire": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "@aspire/mcp-server"],
|
"args": ["-y", "@aspire/mcp-server"],
|
||||||
"env": {
|
"env": {
|
||||||
"ASPIRE_DASHBOARD_URL": "${ASPIRE_DASHBOARD_URL}"
|
"ASPIRE_DASHBOARD_URL": "${ASPIRE_DASHBOARD_URL}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Authentication Notes
|
## Authentication Notes
|
||||||
|
|
||||||
- **GitHub MCP requires a separate token** from the `gh` CLI auth. Generate at https://github.com/settings/tokens
|
- **GitHub MCP requires a separate token** from the `gh` CLI auth. Generate at https://github.com/settings/tokens
|
||||||
- **Trello requires API key + token** from https://trello.com/power-ups/admin
|
- **Trello requires API key + token** from https://trello.com/power-ups/admin
|
||||||
- **Azure requires service principal credentials** — see Azure docs for setup
|
- **Azure requires service principal credentials** — see Azure docs for setup
|
||||||
- **Aspire uses the dashboard URL** — typically `http://localhost:18888` during local dev
|
- **Aspire uses the dashboard URL** — typically `http://localhost:18888` during local dev
|
||||||
|
|
||||||
Auth is a real blocker for some MCP servers. Users need separate tokens for GitHub MCP, Azure MCP, Trello MCP, etc. This is a documentation problem, not a code problem.
|
Auth is a real blocker for some MCP servers. Users need separate tokens for GitHub MCP, Azure MCP, Trello MCP, etc. This is a documentation problem, not a code problem.
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
# Multi-Agent Artifact Format
|
# Multi-Agent Artifact Format
|
||||||
|
|
||||||
When multiple agents contribute to a final artifact (document, analysis, design), use this format. The assembled result must include:
|
When multiple agents contribute to a final artifact (document, analysis, design), use this format. The assembled result must include:
|
||||||
|
|
||||||
- Termination condition
|
- Termination condition
|
||||||
- Constraint budgets (if active)
|
- Constraint budgets (if active)
|
||||||
- Reviewer verdicts (if any)
|
- Reviewer verdicts (if any)
|
||||||
- Raw agent outputs appendix
|
- Raw agent outputs appendix
|
||||||
|
|
||||||
## Assembly Structure
|
## Assembly Structure
|
||||||
|
|
||||||
The assembled result goes at the top. Below it, include:
|
The assembled result goes at the top. Below it, include:
|
||||||
|
|
||||||
```
|
```
|
||||||
## APPENDIX: RAW AGENT OUTPUTS
|
## APPENDIX: RAW AGENT OUTPUTS
|
||||||
|
|
||||||
### {Name} ({Role}) — Raw Output
|
### {Name} ({Role}) — Raw Output
|
||||||
{Paste agent's verbatim response here, unedited}
|
{Paste agent's verbatim response here, unedited}
|
||||||
|
|
||||||
### {Name} ({Role}) — Raw Output
|
### {Name} ({Role}) — Raw Output
|
||||||
{Paste agent's verbatim response here, unedited}
|
{Paste agent's verbatim response here, unedited}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Appendix Rules
|
## Appendix Rules
|
||||||
|
|
||||||
This appendix is for diagnostic integrity. Do not edit, summarize, or polish the raw outputs. The Coordinator may not rewrite raw agent outputs; it may only paste them verbatim and assemble the final artifact above.
|
This appendix is for diagnostic integrity. Do not edit, summarize, or polish the raw outputs. The Coordinator may not rewrite raw agent outputs; it may only paste them verbatim and assemble the final artifact above.
|
||||||
|
|
||||||
See `.squad/templates/run-output.md` for the complete output format template.
|
See `.squad/templates/run-output.md` for the complete output format template.
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
# Orchestration Log Entry
|
# Orchestration Log Entry
|
||||||
|
|
||||||
> One file per agent spawn. Saved to `.squad/orchestration-log/{timestamp}-{agent-name}.md`
|
> One file per agent spawn. Saved to `.squad/orchestration-log/{timestamp}-{agent-name}.md`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### {timestamp} — {task summary}
|
### {timestamp} — {task summary}
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|-------|-------|
|
|-------|-------|
|
||||||
| **Agent routed** | {Name} ({Role}) |
|
| **Agent routed** | {Name} ({Role}) |
|
||||||
| **Why chosen** | {Routing rationale — what in the request matched this agent} |
|
| **Why chosen** | {Routing rationale — what in the request matched this agent} |
|
||||||
| **Mode** | {`background` / `sync`} |
|
| **Mode** | {`background` / `sync`} |
|
||||||
| **Why this mode** | {Brief reason — e.g., "No hard data dependencies" or "User needs to approve architecture"} |
|
| **Why this mode** | {Brief reason — e.g., "No hard data dependencies" or "User needs to approve architecture"} |
|
||||||
| **Files authorized to read** | {Exact file paths the agent was told to read} |
|
| **Files authorized to read** | {Exact file paths the agent was told to read} |
|
||||||
| **File(s) agent must produce** | {Exact file paths the agent is expected to create or modify} |
|
| **File(s) agent must produce** | {Exact file paths the agent is expected to create or modify} |
|
||||||
| **Outcome** | {Completed / Rejected by {Reviewer} / Escalated} |
|
| **Outcome** | {Completed / Rejected by {Reviewer} / Escalated} |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
1. **One file per agent spawn.** Named `{timestamp}-{agent-name}.md`.
|
1. **One file per agent spawn.** Named `{timestamp}-{agent-name}.md`.
|
||||||
2. **Log BEFORE spawning.** The entry must exist before the agent runs.
|
2. **Log BEFORE spawning.** The entry must exist before the agent runs.
|
||||||
3. **Update outcome AFTER the agent completes.** Fill in the Outcome field.
|
3. **Update outcome AFTER the agent completes.** Fill in the Outcome field.
|
||||||
4. **Never delete or edit past entries.** Append-only.
|
4. **Never delete or edit past entries.** Append-only.
|
||||||
5. **If a reviewer rejects work,** log the rejection as a new entry with the revision agent.
|
5. **If a reviewer rejects work,** log the rejection as a new entry with the revision agent.
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"type": "commonjs"
|
"type": "commonjs"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,49 @@
|
|||||||
# Plugin Marketplace
|
# Plugin Marketplace
|
||||||
|
|
||||||
Plugins are curated agent templates, skills, instructions, and prompts shared by the community via GitHub repositories (e.g., `github/awesome-copilot`, `anthropics/skills`). They provide ready-made expertise for common domains — cloud platforms, frameworks, testing strategies, etc.
|
Plugins are curated agent templates, skills, instructions, and prompts shared by the community via GitHub repositories (e.g., `github/awesome-copilot`, `anthropics/skills`). They provide ready-made expertise for common domains — cloud platforms, frameworks, testing strategies, etc.
|
||||||
|
|
||||||
## Marketplace State
|
## Marketplace State
|
||||||
|
|
||||||
Registered marketplace sources are stored in `.squad/plugins/marketplaces.json`:
|
Registered marketplace sources are stored in `.squad/plugins/marketplaces.json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"marketplaces": [
|
"marketplaces": [
|
||||||
{
|
{
|
||||||
"name": "awesome-copilot",
|
"name": "awesome-copilot",
|
||||||
"source": "github/awesome-copilot",
|
"source": "github/awesome-copilot",
|
||||||
"added_at": "2026-02-14T00:00:00Z"
|
"added_at": "2026-02-14T00:00:00Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## CLI Commands
|
## CLI Commands
|
||||||
|
|
||||||
Users manage marketplaces via the CLI:
|
Users manage marketplaces via the CLI:
|
||||||
- `squad plugin marketplace add {owner/repo}` — Register a GitHub repo as a marketplace source
|
- `squad plugin marketplace add {owner/repo}` — Register a GitHub repo as a marketplace source
|
||||||
- `squad plugin marketplace remove {name}` — Remove a registered marketplace
|
- `squad plugin marketplace remove {name}` — Remove a registered marketplace
|
||||||
- `squad plugin marketplace list` — List registered marketplaces
|
- `squad plugin marketplace list` — List registered marketplaces
|
||||||
- `squad plugin marketplace browse {name}` — List available plugins in a marketplace
|
- `squad plugin marketplace browse {name}` — List available plugins in a marketplace
|
||||||
|
|
||||||
## When to Browse
|
## When to Browse
|
||||||
|
|
||||||
During the **Adding Team Members** flow, AFTER allocating a name but BEFORE generating the charter:
|
During the **Adding Team Members** flow, AFTER allocating a name but BEFORE generating the charter:
|
||||||
|
|
||||||
1. Read `.squad/plugins/marketplaces.json`. If the file doesn't exist or `marketplaces` is empty, skip silently.
|
1. Read `.squad/plugins/marketplaces.json`. If the file doesn't exist or `marketplaces` is empty, skip silently.
|
||||||
2. For each registered marketplace, search for plugins whose name or description matches the new member's role or domain keywords.
|
2. For each registered marketplace, search for plugins whose name or description matches the new member's role or domain keywords.
|
||||||
3. Present matching plugins to the user: *"Found '{plugin-name}' in {marketplace} marketplace — want me to install it as a skill for {CastName}?"*
|
3. Present matching plugins to the user: *"Found '{plugin-name}' in {marketplace} marketplace — want me to install it as a skill for {CastName}?"*
|
||||||
4. If the user accepts, install the plugin (see below). If they decline or skip, proceed without it.
|
4. If the user accepts, install the plugin (see below). If they decline or skip, proceed without it.
|
||||||
|
|
||||||
## How to Install a Plugin
|
## How to Install a Plugin
|
||||||
|
|
||||||
1. Read the plugin content from the marketplace repository (the plugin's `SKILL.md` or equivalent).
|
1. Read the plugin content from the marketplace repository (the plugin's `SKILL.md` or equivalent).
|
||||||
2. Copy it into the agent's skills directory: `.squad/skills/{plugin-name}/SKILL.md`
|
2. Copy it into the agent's skills directory: `.squad/skills/{plugin-name}/SKILL.md`
|
||||||
3. If the plugin includes charter-level instructions (role boundaries, tool preferences), merge those into the agent's `charter.md`.
|
3. If the plugin includes charter-level instructions (role boundaries, tool preferences), merge those into the agent's `charter.md`.
|
||||||
4. Log the installation in the agent's `history.md`: *"📦 Plugin '{plugin-name}' installed from {marketplace}."*
|
4. Log the installation in the agent's `history.md`: *"📦 Plugin '{plugin-name}' installed from {marketplace}."*
|
||||||
|
|
||||||
## Graceful Degradation
|
## Graceful Degradation
|
||||||
|
|
||||||
- **No marketplaces configured:** Skip the marketplace check entirely. No warning, no prompt.
|
- **No marketplaces configured:** Skip the marketplace check entirely. No warning, no prompt.
|
||||||
- **Marketplace unreachable:** Warn the user (*"⚠ Couldn't reach {marketplace} — continuing without it"*) and proceed with team member creation normally.
|
- **Marketplace unreachable:** Warn the user (*"⚠ Couldn't reach {marketplace} — continuing without it"*) and proceed with team member creation normally.
|
||||||
- **No matching plugins:** Inform the user (*"No matching plugins found in configured marketplaces"*) and proceed.
|
- **No matching plugins:** Inform the user (*"No matching plugins found in configured marketplaces"*) and proceed.
|
||||||
|
|||||||
@@ -1,313 +1,313 @@
|
|||||||
# Ralph Circuit Breaker — Model Rate Limit Fallback
|
# Ralph Circuit Breaker — Model Rate Limit Fallback
|
||||||
|
|
||||||
> Classic circuit breaker pattern (Hystrix / Polly / Resilience4j) applied to Copilot model selection.
|
> Classic circuit breaker pattern (Hystrix / Polly / Resilience4j) applied to Copilot model selection.
|
||||||
> When the preferred model hits rate limits, Ralph automatically degrades to free-tier models, then self-heals.
|
> When the preferred model hits rate limits, Ralph automatically degrades to free-tier models, then self-heals.
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
When running multiple Ralph instances across repos, Copilot model rate limits cause cascading failures.
|
When running multiple Ralph instances across repos, Copilot model rate limits cause cascading failures.
|
||||||
All Ralphs fail simultaneously when the preferred model (e.g., `claude-sonnet-4.6`) hits quota.
|
All Ralphs fail simultaneously when the preferred model (e.g., `claude-sonnet-4.6`) hits quota.
|
||||||
|
|
||||||
Premium models burn quota fast:
|
Premium models burn quota fast:
|
||||||
| Model | Multiplier | Risk |
|
| Model | Multiplier | Risk |
|
||||||
|-------|-----------|------|
|
|-------|-----------|------|
|
||||||
| `claude-sonnet-4.6` | 1x | Moderate with many Ralphs |
|
| `claude-sonnet-4.6` | 1x | Moderate with many Ralphs |
|
||||||
| `claude-opus-4.6` | 10x | High |
|
| `claude-opus-4.6` | 10x | High |
|
||||||
| `gpt-5.4` | 50x | Very high |
|
| `gpt-5.4` | 50x | Very high |
|
||||||
| `gpt-5.4-mini` | **0x** | **Free — unlimited** |
|
| `gpt-5.4-mini` | **0x** | **Free — unlimited** |
|
||||||
| `gpt-5-mini` | **0x** | **Free — unlimited** |
|
| `gpt-5-mini` | **0x** | **Free — unlimited** |
|
||||||
| `gpt-4.1` | **0x** | **Free — unlimited** |
|
| `gpt-4.1` | **0x** | **Free — unlimited** |
|
||||||
|
|
||||||
## Circuit Breaker States
|
## Circuit Breaker States
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────┐ rate limit error ┌────────┐
|
┌─────────┐ rate limit error ┌────────┐
|
||||||
│ CLOSED │ ───────────────────► │ OPEN │
|
│ CLOSED │ ───────────────────► │ OPEN │
|
||||||
│ (normal)│ │(fallback)│
|
│ (normal)│ │(fallback)│
|
||||||
└────┬────┘ ◄──────────────── └────┬────┘
|
└────┬────┘ ◄──────────────── └────┬────┘
|
||||||
│ 2 consecutive │
|
│ 2 consecutive │
|
||||||
│ successes │ cooldown expires
|
│ successes │ cooldown expires
|
||||||
│ ▼
|
│ ▼
|
||||||
│ ┌──────────┐
|
│ ┌──────────┐
|
||||||
└───── success ◄──────── │HALF-OPEN │
|
└───── success ◄──────── │HALF-OPEN │
|
||||||
(close) │ (testing) │
|
(close) │ (testing) │
|
||||||
└──────────┘
|
└──────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### CLOSED (normal operation)
|
### CLOSED (normal operation)
|
||||||
- Use preferred model from config
|
- Use preferred model from config
|
||||||
- Every successful response confirms circuit stays closed
|
- Every successful response confirms circuit stays closed
|
||||||
- On rate limit error → transition to OPEN
|
- On rate limit error → transition to OPEN
|
||||||
|
|
||||||
### OPEN (rate limited — fallback active)
|
### OPEN (rate limited — fallback active)
|
||||||
- Fall back through the free-tier model chain:
|
- Fall back through the free-tier model chain:
|
||||||
1. `gpt-5.4-mini`
|
1. `gpt-5.4-mini`
|
||||||
2. `gpt-5-mini`
|
2. `gpt-5-mini`
|
||||||
3. `gpt-4.1`
|
3. `gpt-4.1`
|
||||||
- Start cooldown timer (default: 10 minutes)
|
- Start cooldown timer (default: 10 minutes)
|
||||||
- When cooldown expires → transition to HALF-OPEN
|
- When cooldown expires → transition to HALF-OPEN
|
||||||
|
|
||||||
### HALF-OPEN (testing recovery)
|
### HALF-OPEN (testing recovery)
|
||||||
- Try preferred model again
|
- Try preferred model again
|
||||||
- If 2 consecutive successes → transition to CLOSED
|
- If 2 consecutive successes → transition to CLOSED
|
||||||
- If rate limit error → back to OPEN, reset cooldown
|
- If rate limit error → back to OPEN, reset cooldown
|
||||||
|
|
||||||
## State File: `.squad/ralph-circuit-breaker.json`
|
## State File: `.squad/ralph-circuit-breaker.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"state": "closed",
|
"state": "closed",
|
||||||
"preferredModel": "claude-sonnet-4.6",
|
"preferredModel": "claude-sonnet-4.6",
|
||||||
"fallbackChain": ["gpt-5.4-mini", "gpt-5-mini", "gpt-4.1"],
|
"fallbackChain": ["gpt-5.4-mini", "gpt-5-mini", "gpt-4.1"],
|
||||||
"currentFallbackIndex": 0,
|
"currentFallbackIndex": 0,
|
||||||
"cooldownMinutes": 10,
|
"cooldownMinutes": 10,
|
||||||
"openedAt": null,
|
"openedAt": null,
|
||||||
"halfOpenSuccesses": 0,
|
"halfOpenSuccesses": 0,
|
||||||
"consecutiveFailures": 0,
|
"consecutiveFailures": 0,
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"totalFallbacks": 0,
|
"totalFallbacks": 0,
|
||||||
"totalRecoveries": 0,
|
"totalRecoveries": 0,
|
||||||
"lastFallbackAt": null,
|
"lastFallbackAt": null,
|
||||||
"lastRecoveryAt": null
|
"lastRecoveryAt": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## PowerShell Functions
|
## PowerShell Functions
|
||||||
|
|
||||||
Paste these into your `ralph-watch.ps1` or source them from a shared module.
|
Paste these into your `ralph-watch.ps1` or source them from a shared module.
|
||||||
|
|
||||||
### `Get-CircuitBreakerState`
|
### `Get-CircuitBreakerState`
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
function Get-CircuitBreakerState {
|
function Get-CircuitBreakerState {
|
||||||
param([string]$StateFile = ".squad/ralph-circuit-breaker.json")
|
param([string]$StateFile = ".squad/ralph-circuit-breaker.json")
|
||||||
|
|
||||||
if (-not (Test-Path $StateFile)) {
|
if (-not (Test-Path $StateFile)) {
|
||||||
$default = @{
|
$default = @{
|
||||||
state = "closed"
|
state = "closed"
|
||||||
preferredModel = "claude-sonnet-4.6"
|
preferredModel = "claude-sonnet-4.6"
|
||||||
fallbackChain = @("gpt-5.4-mini", "gpt-5-mini", "gpt-4.1")
|
fallbackChain = @("gpt-5.4-mini", "gpt-5-mini", "gpt-4.1")
|
||||||
currentFallbackIndex = 0
|
currentFallbackIndex = 0
|
||||||
cooldownMinutes = 10
|
cooldownMinutes = 10
|
||||||
openedAt = $null
|
openedAt = $null
|
||||||
halfOpenSuccesses = 0
|
halfOpenSuccesses = 0
|
||||||
consecutiveFailures = 0
|
consecutiveFailures = 0
|
||||||
metrics = @{
|
metrics = @{
|
||||||
totalFallbacks = 0
|
totalFallbacks = 0
|
||||||
totalRecoveries = 0
|
totalRecoveries = 0
|
||||||
lastFallbackAt = $null
|
lastFallbackAt = $null
|
||||||
lastRecoveryAt = $null
|
lastRecoveryAt = $null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$default | ConvertTo-Json -Depth 3 | Set-Content $StateFile
|
$default | ConvertTo-Json -Depth 3 | Set-Content $StateFile
|
||||||
return $default
|
return $default
|
||||||
}
|
}
|
||||||
|
|
||||||
return (Get-Content $StateFile -Raw | ConvertFrom-Json)
|
return (Get-Content $StateFile -Raw | ConvertFrom-Json)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `Save-CircuitBreakerState`
|
### `Save-CircuitBreakerState`
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
function Save-CircuitBreakerState {
|
function Save-CircuitBreakerState {
|
||||||
param(
|
param(
|
||||||
[object]$State,
|
[object]$State,
|
||||||
[string]$StateFile = ".squad/ralph-circuit-breaker.json"
|
[string]$StateFile = ".squad/ralph-circuit-breaker.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
$State | ConvertTo-Json -Depth 3 | Set-Content $StateFile
|
$State | ConvertTo-Json -Depth 3 | Set-Content $StateFile
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `Get-CurrentModel`
|
### `Get-CurrentModel`
|
||||||
|
|
||||||
Returns the model Ralph should use right now, based on circuit state.
|
Returns the model Ralph should use right now, based on circuit state.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
function Get-CurrentModel {
|
function Get-CurrentModel {
|
||||||
param([string]$StateFile = ".squad/ralph-circuit-breaker.json")
|
param([string]$StateFile = ".squad/ralph-circuit-breaker.json")
|
||||||
|
|
||||||
$cb = Get-CircuitBreakerState -StateFile $StateFile
|
$cb = Get-CircuitBreakerState -StateFile $StateFile
|
||||||
|
|
||||||
switch ($cb.state) {
|
switch ($cb.state) {
|
||||||
"closed" {
|
"closed" {
|
||||||
return $cb.preferredModel
|
return $cb.preferredModel
|
||||||
}
|
}
|
||||||
"open" {
|
"open" {
|
||||||
# Check if cooldown has expired
|
# Check if cooldown has expired
|
||||||
if ($cb.openedAt) {
|
if ($cb.openedAt) {
|
||||||
$opened = [DateTime]::Parse($cb.openedAt)
|
$opened = [DateTime]::Parse($cb.openedAt)
|
||||||
$elapsed = (Get-Date) - $opened
|
$elapsed = (Get-Date) - $opened
|
||||||
if ($elapsed.TotalMinutes -ge $cb.cooldownMinutes) {
|
if ($elapsed.TotalMinutes -ge $cb.cooldownMinutes) {
|
||||||
# Transition to half-open
|
# Transition to half-open
|
||||||
$cb.state = "half-open"
|
$cb.state = "half-open"
|
||||||
$cb.halfOpenSuccesses = 0
|
$cb.halfOpenSuccesses = 0
|
||||||
Save-CircuitBreakerState -State $cb -StateFile $StateFile
|
Save-CircuitBreakerState -State $cb -StateFile $StateFile
|
||||||
Write-Host " [circuit-breaker] Cooldown expired. Testing preferred model..." -ForegroundColor Yellow
|
Write-Host " [circuit-breaker] Cooldown expired. Testing preferred model..." -ForegroundColor Yellow
|
||||||
return $cb.preferredModel
|
return $cb.preferredModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# Still in cooldown — use fallback
|
# Still in cooldown — use fallback
|
||||||
$idx = [Math]::Min($cb.currentFallbackIndex, $cb.fallbackChain.Count - 1)
|
$idx = [Math]::Min($cb.currentFallbackIndex, $cb.fallbackChain.Count - 1)
|
||||||
return $cb.fallbackChain[$idx]
|
return $cb.fallbackChain[$idx]
|
||||||
}
|
}
|
||||||
"half-open" {
|
"half-open" {
|
||||||
return $cb.preferredModel
|
return $cb.preferredModel
|
||||||
}
|
}
|
||||||
default {
|
default {
|
||||||
return $cb.preferredModel
|
return $cb.preferredModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `Update-CircuitBreakerOnSuccess`
|
### `Update-CircuitBreakerOnSuccess`
|
||||||
|
|
||||||
Call after every successful model response.
|
Call after every successful model response.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
function Update-CircuitBreakerOnSuccess {
|
function Update-CircuitBreakerOnSuccess {
|
||||||
param([string]$StateFile = ".squad/ralph-circuit-breaker.json")
|
param([string]$StateFile = ".squad/ralph-circuit-breaker.json")
|
||||||
|
|
||||||
$cb = Get-CircuitBreakerState -StateFile $StateFile
|
$cb = Get-CircuitBreakerState -StateFile $StateFile
|
||||||
$cb.consecutiveFailures = 0
|
$cb.consecutiveFailures = 0
|
||||||
|
|
||||||
if ($cb.state -eq "half-open") {
|
if ($cb.state -eq "half-open") {
|
||||||
$cb.halfOpenSuccesses++
|
$cb.halfOpenSuccesses++
|
||||||
if ($cb.halfOpenSuccesses -ge 2) {
|
if ($cb.halfOpenSuccesses -ge 2) {
|
||||||
# Recovery! Close the circuit
|
# Recovery! Close the circuit
|
||||||
$cb.state = "closed"
|
$cb.state = "closed"
|
||||||
$cb.openedAt = $null
|
$cb.openedAt = $null
|
||||||
$cb.halfOpenSuccesses = 0
|
$cb.halfOpenSuccesses = 0
|
||||||
$cb.currentFallbackIndex = 0
|
$cb.currentFallbackIndex = 0
|
||||||
$cb.metrics.totalRecoveries++
|
$cb.metrics.totalRecoveries++
|
||||||
$cb.metrics.lastRecoveryAt = (Get-Date).ToString("o")
|
$cb.metrics.lastRecoveryAt = (Get-Date).ToString("o")
|
||||||
Save-CircuitBreakerState -State $cb -StateFile $StateFile
|
Save-CircuitBreakerState -State $cb -StateFile $StateFile
|
||||||
Write-Host " [circuit-breaker] RECOVERED — back to preferred model ($($cb.preferredModel))" -ForegroundColor Green
|
Write-Host " [circuit-breaker] RECOVERED — back to preferred model ($($cb.preferredModel))" -ForegroundColor Green
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Save-CircuitBreakerState -State $cb -StateFile $StateFile
|
Save-CircuitBreakerState -State $cb -StateFile $StateFile
|
||||||
Write-Host " [circuit-breaker] Half-open success $($cb.halfOpenSuccesses)/2" -ForegroundColor Yellow
|
Write-Host " [circuit-breaker] Half-open success $($cb.halfOpenSuccesses)/2" -ForegroundColor Yellow
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
# closed state — nothing to do
|
# closed state — nothing to do
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `Update-CircuitBreakerOnRateLimit`
|
### `Update-CircuitBreakerOnRateLimit`
|
||||||
|
|
||||||
Call when a model response indicates rate limiting (HTTP 429 or error message containing "rate limit").
|
Call when a model response indicates rate limiting (HTTP 429 or error message containing "rate limit").
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
function Update-CircuitBreakerOnRateLimit {
|
function Update-CircuitBreakerOnRateLimit {
|
||||||
param([string]$StateFile = ".squad/ralph-circuit-breaker.json")
|
param([string]$StateFile = ".squad/ralph-circuit-breaker.json")
|
||||||
|
|
||||||
$cb = Get-CircuitBreakerState -StateFile $StateFile
|
$cb = Get-CircuitBreakerState -StateFile $StateFile
|
||||||
$cb.consecutiveFailures++
|
$cb.consecutiveFailures++
|
||||||
|
|
||||||
if ($cb.state -eq "closed" -or $cb.state -eq "half-open") {
|
if ($cb.state -eq "closed" -or $cb.state -eq "half-open") {
|
||||||
# Open the circuit
|
# Open the circuit
|
||||||
$cb.state = "open"
|
$cb.state = "open"
|
||||||
$cb.openedAt = (Get-Date).ToString("o")
|
$cb.openedAt = (Get-Date).ToString("o")
|
||||||
$cb.halfOpenSuccesses = 0
|
$cb.halfOpenSuccesses = 0
|
||||||
$cb.currentFallbackIndex = 0
|
$cb.currentFallbackIndex = 0
|
||||||
$cb.metrics.totalFallbacks++
|
$cb.metrics.totalFallbacks++
|
||||||
$cb.metrics.lastFallbackAt = (Get-Date).ToString("o")
|
$cb.metrics.lastFallbackAt = (Get-Date).ToString("o")
|
||||||
Save-CircuitBreakerState -State $cb -StateFile $StateFile
|
Save-CircuitBreakerState -State $cb -StateFile $StateFile
|
||||||
|
|
||||||
$fallbackModel = $cb.fallbackChain[0]
|
$fallbackModel = $cb.fallbackChain[0]
|
||||||
Write-Host " [circuit-breaker] RATE LIMITED — falling back to $fallbackModel (cooldown: $($cb.cooldownMinutes)m)" -ForegroundColor Red
|
Write-Host " [circuit-breaker] RATE LIMITED — falling back to $fallbackModel (cooldown: $($cb.cooldownMinutes)m)" -ForegroundColor Red
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($cb.state -eq "open") {
|
if ($cb.state -eq "open") {
|
||||||
# Already open — try next fallback in chain if current one also fails
|
# Already open — try next fallback in chain if current one also fails
|
||||||
if ($cb.currentFallbackIndex -lt ($cb.fallbackChain.Count - 1)) {
|
if ($cb.currentFallbackIndex -lt ($cb.fallbackChain.Count - 1)) {
|
||||||
$cb.currentFallbackIndex++
|
$cb.currentFallbackIndex++
|
||||||
$nextModel = $cb.fallbackChain[$cb.currentFallbackIndex]
|
$nextModel = $cb.fallbackChain[$cb.currentFallbackIndex]
|
||||||
Write-Host " [circuit-breaker] Fallback also limited — trying $nextModel" -ForegroundColor Red
|
Write-Host " [circuit-breaker] Fallback also limited — trying $nextModel" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
# Reset cooldown timer
|
# Reset cooldown timer
|
||||||
$cb.openedAt = (Get-Date).ToString("o")
|
$cb.openedAt = (Get-Date).ToString("o")
|
||||||
Save-CircuitBreakerState -State $cb -StateFile $StateFile
|
Save-CircuitBreakerState -State $cb -StateFile $StateFile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Integration with ralph-watch.ps1
|
## Integration with ralph-watch.ps1
|
||||||
|
|
||||||
In your Ralph polling loop, wrap the model selection:
|
In your Ralph polling loop, wrap the model selection:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# At the top of your polling loop
|
# At the top of your polling loop
|
||||||
$model = Get-CurrentModel
|
$model = Get-CurrentModel
|
||||||
|
|
||||||
# When invoking copilot CLI
|
# When invoking copilot CLI
|
||||||
$result = copilot-cli --model $model ...
|
$result = copilot-cli --model $model ...
|
||||||
|
|
||||||
# After the call
|
# After the call
|
||||||
if ($result -match "rate.?limit" -or $LASTEXITCODE -eq 429) {
|
if ($result -match "rate.?limit" -or $LASTEXITCODE -eq 429) {
|
||||||
Update-CircuitBreakerOnRateLimit
|
Update-CircuitBreakerOnRateLimit
|
||||||
} else {
|
} else {
|
||||||
Update-CircuitBreakerOnSuccess
|
Update-CircuitBreakerOnSuccess
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Full integration example
|
### Full integration example
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Source the circuit breaker functions
|
# Source the circuit breaker functions
|
||||||
. .squad-templates/ralph-circuit-breaker-functions.ps1
|
. .squad-templates/ralph-circuit-breaker-functions.ps1
|
||||||
|
|
||||||
while ($true) {
|
while ($true) {
|
||||||
$model = Get-CurrentModel
|
$model = Get-CurrentModel
|
||||||
Write-Host "Polling with model: $model"
|
Write-Host "Polling with model: $model"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
# Your existing Ralph logic here, but pass $model
|
# Your existing Ralph logic here, but pass $model
|
||||||
$response = Invoke-RalphCycle -Model $model
|
$response = Invoke-RalphCycle -Model $model
|
||||||
|
|
||||||
# Success path
|
# Success path
|
||||||
Update-CircuitBreakerOnSuccess
|
Update-CircuitBreakerOnSuccess
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
if ($_.Exception.Message -match "rate.?limit|429|quota|Too Many Requests") {
|
if ($_.Exception.Message -match "rate.?limit|429|quota|Too Many Requests") {
|
||||||
Update-CircuitBreakerOnRateLimit
|
Update-CircuitBreakerOnRateLimit
|
||||||
# Retry immediately with fallback model
|
# Retry immediately with fallback model
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
# Other errors — handle normally
|
# Other errors — handle normally
|
||||||
throw
|
throw
|
||||||
}
|
}
|
||||||
|
|
||||||
Start-Sleep -Seconds $pollInterval
|
Start-Sleep -Seconds $pollInterval
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Override defaults by editing `.squad/ralph-circuit-breaker.json`:
|
Override defaults by editing `.squad/ralph-circuit-breaker.json`:
|
||||||
|
|
||||||
| Field | Default | Description |
|
| Field | Default | Description |
|
||||||
|-------|---------|-------------|
|
|-------|---------|-------------|
|
||||||
| `preferredModel` | `claude-sonnet-4.6` | Model to use when circuit is closed |
|
| `preferredModel` | `claude-sonnet-4.6` | Model to use when circuit is closed |
|
||||||
| `fallbackChain` | `["gpt-5.4-mini", "gpt-5-mini", "gpt-4.1"]` | Ordered fallback models (all free-tier) |
|
| `fallbackChain` | `["gpt-5.4-mini", "gpt-5-mini", "gpt-4.1"]` | Ordered fallback models (all free-tier) |
|
||||||
| `cooldownMinutes` | `10` | How long to wait before testing recovery |
|
| `cooldownMinutes` | `10` | How long to wait before testing recovery |
|
||||||
|
|
||||||
## Metrics
|
## Metrics
|
||||||
|
|
||||||
The state file tracks operational metrics:
|
The state file tracks operational metrics:
|
||||||
|
|
||||||
- **totalFallbacks** — How many times the circuit opened
|
- **totalFallbacks** — How many times the circuit opened
|
||||||
- **totalRecoveries** — How many times it recovered to preferred model
|
- **totalRecoveries** — How many times it recovered to preferred model
|
||||||
- **lastFallbackAt** — ISO timestamp of last rate limit event
|
- **lastFallbackAt** — ISO timestamp of last rate limit event
|
||||||
- **lastRecoveryAt** — ISO timestamp of last successful recovery
|
- **lastRecoveryAt** — ISO timestamp of last successful recovery
|
||||||
|
|
||||||
Query metrics with:
|
Query metrics with:
|
||||||
```powershell
|
```powershell
|
||||||
$cb = Get-Content .squad/ralph-circuit-breaker.json | ConvertFrom-Json
|
$cb = Get-Content .squad/ralph-circuit-breaker.json | ConvertFrom-Json
|
||||||
Write-Host "Fallbacks: $($cb.metrics.totalFallbacks) | Recoveries: $($cb.metrics.totalRecoveries)"
|
Write-Host "Fallbacks: $($cb.metrics.totalFallbacks) | Recoveries: $($cb.metrics.totalRecoveries)"
|
||||||
```
|
```
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,37 +1,37 @@
|
|||||||
# Raw Agent Output — Appendix Format
|
# Raw Agent Output — Appendix Format
|
||||||
|
|
||||||
> This template defines the format for the `## APPENDIX: RAW AGENT OUTPUTS` section
|
> This template defines the format for the `## APPENDIX: RAW AGENT OUTPUTS` section
|
||||||
> in any multi-agent artifact.
|
> in any multi-agent artifact.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
1. **Verbatim only.** Paste the agent's response exactly as returned. No edits.
|
1. **Verbatim only.** Paste the agent's response exactly as returned. No edits.
|
||||||
2. **No summarizing.** Do not condense, paraphrase, or rephrase any part of the output.
|
2. **No summarizing.** Do not condense, paraphrase, or rephrase any part of the output.
|
||||||
3. **No rewriting.** Do not fix typos, grammar, formatting, or style.
|
3. **No rewriting.** Do not fix typos, grammar, formatting, or style.
|
||||||
4. **No code fences around the entire output.** The raw output is pasted as-is, not wrapped in ``` blocks.
|
4. **No code fences around the entire output.** The raw output is pasted as-is, not wrapped in ``` blocks.
|
||||||
5. **One section per agent.** Each agent that contributed gets its own heading.
|
5. **One section per agent.** Each agent that contributed gets its own heading.
|
||||||
6. **Order matches work order.** List agents in the order they were spawned.
|
6. **Order matches work order.** List agents in the order they were spawned.
|
||||||
7. **Include all outputs.** Even if an agent's work was rejected, include their output for diagnostic traceability.
|
7. **Include all outputs.** Even if an agent's work was rejected, include their output for diagnostic traceability.
|
||||||
|
|
||||||
## Format
|
## Format
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
## APPENDIX: RAW AGENT OUTPUTS
|
## APPENDIX: RAW AGENT OUTPUTS
|
||||||
|
|
||||||
### {Name} ({Role}) — Raw Output
|
### {Name} ({Role}) — Raw Output
|
||||||
|
|
||||||
{Paste agent's verbatim response here, unedited}
|
{Paste agent's verbatim response here, unedited}
|
||||||
|
|
||||||
### {Name} ({Role}) — Raw Output
|
### {Name} ({Role}) — Raw Output
|
||||||
|
|
||||||
{Paste agent's verbatim response here, unedited}
|
{Paste agent's verbatim response here, unedited}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why This Exists
|
## Why This Exists
|
||||||
|
|
||||||
The appendix provides diagnostic integrity. It lets anyone verify:
|
The appendix provides diagnostic integrity. It lets anyone verify:
|
||||||
- What each agent actually said (vs. what the Coordinator assembled)
|
- What each agent actually said (vs. what the Coordinator assembled)
|
||||||
- Whether the Coordinator faithfully represented agent work
|
- Whether the Coordinator faithfully represented agent work
|
||||||
- What was lost or changed in synthesis
|
- What was lost or changed in synthesis
|
||||||
|
|
||||||
Without raw outputs, multi-agent collaboration is unauditable.
|
Without raw outputs, multi-agent collaboration is unauditable.
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
# Team Roster
|
# Team Roster
|
||||||
|
|
||||||
> {One-line project description}
|
> {One-line project description}
|
||||||
|
|
||||||
## Coordinator
|
## Coordinator
|
||||||
|
|
||||||
| Name | Role | Notes |
|
| Name | Role | Notes |
|
||||||
|------|------|-------|
|
|------|------|-------|
|
||||||
| Squad | Coordinator | Routes work, enforces handoffs and reviewer gates. Does not generate domain artifacts. |
|
| Squad | Coordinator | Routes work, enforces handoffs and reviewer gates. Does not generate domain artifacts. |
|
||||||
|
|
||||||
## Members
|
## Members
|
||||||
|
|
||||||
| Name | Role | Charter | Status |
|
| Name | Role | Charter | Status |
|
||||||
|------|------|---------|--------|
|
|------|------|---------|--------|
|
||||||
| {Name} | {Role} | `.squad/agents/{name}/charter.md` | ✅ Active |
|
| {Name} | {Role} | `.squad/agents/{name}/charter.md` | ✅ Active |
|
||||||
| {Name} | {Role} | `.squad/agents/{name}/charter.md` | ✅ Active |
|
| {Name} | {Role} | `.squad/agents/{name}/charter.md` | ✅ Active |
|
||||||
| {Name} | {Role} | `.squad/agents/{name}/charter.md` | ✅ Active |
|
| {Name} | {Role} | `.squad/agents/{name}/charter.md` | ✅ Active |
|
||||||
| {Name} | {Role} | `.squad/agents/{name}/charter.md` | ✅ Active |
|
| {Name} | {Role} | `.squad/agents/{name}/charter.md` | ✅ Active |
|
||||||
| Scribe | Session Logger | `.squad/agents/scribe/charter.md` | 📋 Silent |
|
| Scribe | Session Logger | `.squad/agents/scribe/charter.md` | 📋 Silent |
|
||||||
| Ralph | Work Monitor | — | 🔄 Monitor |
|
| Ralph | Work Monitor | — | 🔄 Monitor |
|
||||||
|
|
||||||
## Coding Agent
|
## Coding Agent
|
||||||
|
|
||||||
<!-- copilot-auto-assign: false -->
|
<!-- copilot-auto-assign: false -->
|
||||||
|
|
||||||
| Name | Role | Charter | Status |
|
| Name | Role | Charter | Status |
|
||||||
|------|------|---------|--------|
|
|------|------|---------|--------|
|
||||||
| @copilot | Coding Agent | — | 🤖 Coding Agent |
|
| @copilot | Coding Agent | — | 🤖 Coding Agent |
|
||||||
|
|
||||||
### Capabilities
|
### Capabilities
|
||||||
|
|
||||||
**🟢 Good fit — auto-route when enabled:**
|
**🟢 Good fit — auto-route when enabled:**
|
||||||
- Bug fixes with clear reproduction steps
|
- Bug fixes with clear reproduction steps
|
||||||
- Test coverage (adding missing tests, fixing flaky tests)
|
- Test coverage (adding missing tests, fixing flaky tests)
|
||||||
- Lint/format fixes and code style cleanup
|
- Lint/format fixes and code style cleanup
|
||||||
- Dependency updates and version bumps
|
- Dependency updates and version bumps
|
||||||
- Small isolated features with clear specs
|
- Small isolated features with clear specs
|
||||||
- Boilerplate/scaffolding generation
|
- Boilerplate/scaffolding generation
|
||||||
- Documentation fixes and README updates
|
- Documentation fixes and README updates
|
||||||
|
|
||||||
**🟡 Needs review — route to @copilot but flag for squad member PR review:**
|
**🟡 Needs review — route to @copilot but flag for squad member PR review:**
|
||||||
- Medium features with clear specs and acceptance criteria
|
- Medium features with clear specs and acceptance criteria
|
||||||
- Refactoring with existing test coverage
|
- Refactoring with existing test coverage
|
||||||
- API endpoint additions following established patterns
|
- API endpoint additions following established patterns
|
||||||
- Migration scripts with well-defined schemas
|
- Migration scripts with well-defined schemas
|
||||||
|
|
||||||
**🔴 Not suitable — route to squad member instead:**
|
**🔴 Not suitable — route to squad member instead:**
|
||||||
- Architecture decisions and system design
|
- Architecture decisions and system design
|
||||||
- Multi-system integration requiring coordination
|
- Multi-system integration requiring coordination
|
||||||
- Ambiguous requirements needing clarification
|
- Ambiguous requirements needing clarification
|
||||||
- Security-critical changes (auth, encryption, access control)
|
- Security-critical changes (auth, encryption, access control)
|
||||||
- Performance-critical paths requiring benchmarking
|
- Performance-critical paths requiring benchmarking
|
||||||
- Changes requiring cross-team discussion
|
- Changes requiring cross-team discussion
|
||||||
|
|
||||||
## Project Context
|
## Project Context
|
||||||
|
|
||||||
- **Owner:** {user name}
|
- **Owner:** {user name}
|
||||||
- **Stack:** {languages, frameworks, tools}
|
- **Stack:** {languages, frameworks, tools}
|
||||||
- **Description:** {what the project does, in one sentence}
|
- **Description:** {what the project does, in one sentence}
|
||||||
- **Created:** {timestamp}
|
- **Created:** {timestamp}
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
# Work Routing
|
# Work Routing
|
||||||
|
|
||||||
How to decide who handles what.
|
How to decide who handles what.
|
||||||
|
|
||||||
## Routing Table
|
## Routing Table
|
||||||
|
|
||||||
| Work Type | Route To | Examples |
|
| Work Type | Route To | Examples |
|
||||||
|-----------|----------|----------|
|
|-----------|----------|----------|
|
||||||
| {domain 1} | {Name} | {example tasks} |
|
| {domain 1} | {Name} | {example tasks} |
|
||||||
| {domain 2} | {Name} | {example tasks} |
|
| {domain 2} | {Name} | {example tasks} |
|
||||||
| {domain 3} | {Name} | {example tasks} |
|
| {domain 3} | {Name} | {example tasks} |
|
||||||
| Code review | {Name} | Review PRs, check quality, suggest improvements |
|
| Code review | {Name} | Review PRs, check quality, suggest improvements |
|
||||||
| Testing | {Name} | Write tests, find edge cases, verify fixes |
|
| Testing | {Name} | Write tests, find edge cases, verify fixes |
|
||||||
| Scope & priorities | {Name} | What to build next, trade-offs, decisions |
|
| Scope & priorities | {Name} | What to build next, trade-offs, decisions |
|
||||||
| Session logging | Scribe | Automatic — never needs routing |
|
| Session logging | Scribe | Automatic — never needs routing |
|
||||||
|
|
||||||
## Issue Routing
|
## Issue Routing
|
||||||
|
|
||||||
| Label | Action | Who |
|
| Label | Action | Who |
|
||||||
|-------|--------|-----|
|
|-------|--------|-----|
|
||||||
| `squad` | Triage: analyze issue, assign `squad:{member}` label | Lead |
|
| `squad` | Triage: analyze issue, assign `squad:{member}` label | Lead |
|
||||||
| `squad:{name}` | Pick up issue and complete the work | Named member |
|
| `squad:{name}` | Pick up issue and complete the work | Named member |
|
||||||
|
|
||||||
### How Issue Assignment Works
|
### How Issue Assignment Works
|
||||||
|
|
||||||
1. When a GitHub issue gets the `squad` label, the **Lead** triages it — analyzing content, assigning the right `squad:{member}` label, and commenting with triage notes.
|
1. When a GitHub issue gets the `squad` label, the **Lead** triages it — analyzing content, assigning the right `squad:{member}` label, and commenting with triage notes.
|
||||||
2. When a `squad:{member}` label is applied, that member picks up the issue in their next session.
|
2. When a `squad:{member}` label is applied, that member picks up the issue in their next session.
|
||||||
3. Members can reassign by removing their label and adding another member's label.
|
3. Members can reassign by removing their label and adding another member's label.
|
||||||
4. The `squad` label is the "inbox" — untriaged issues waiting for Lead review.
|
4. The `squad` label is the "inbox" — untriaged issues waiting for Lead review.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
1. **Eager by default** — spawn all agents who could usefully start work, including anticipatory downstream work.
|
1. **Eager by default** — spawn all agents who could usefully start work, including anticipatory downstream work.
|
||||||
2. **Scribe always runs** after substantial work, always as `mode: "background"`. Never blocks.
|
2. **Scribe always runs** after substantial work, always as `mode: "background"`. Never blocks.
|
||||||
3. **Quick facts → coordinator answers directly.** Don't spawn an agent for "what port does the server run on?"
|
3. **Quick facts → coordinator answers directly.** Don't spawn an agent for "what port does the server run on?"
|
||||||
4. **When two agents could handle it**, pick the one whose domain is the primary concern.
|
4. **When two agents could handle it**, pick the one whose domain is the primary concern.
|
||||||
5. **"Team, ..." → fan-out.** Spawn all relevant agents in parallel as `mode: "background"`.
|
5. **"Team, ..." → fan-out.** Spawn all relevant agents in parallel as `mode: "background"`.
|
||||||
6. **Anticipate downstream work.** If a feature is being built, spawn the tester to write test cases from requirements simultaneously.
|
6. **Anticipate downstream work.** If a feature is being built, spawn the tester to write test cases from requirements simultaneously.
|
||||||
7. **Issue-labeled work** — when a `squad:{member}` label is applied to an issue, route to that member. The Lead handles all `squad` (base label) triage.
|
7. **Issue-labeled work** — when a `squad:{member}` label is applied to an issue, route to that member. The Lead handles all `squad` (base label) triage.
|
||||||
|
|||||||
@@ -1,50 +1,50 @@
|
|||||||
# Run Output — {task title}
|
# Run Output — {task title}
|
||||||
|
|
||||||
> Final assembled artifact from a multi-agent run.
|
> Final assembled artifact from a multi-agent run.
|
||||||
|
|
||||||
## Termination Condition
|
## Termination Condition
|
||||||
|
|
||||||
**Reason:** {One of: User accepted | Reviewer approved | Constraint budget exhausted | Deadlock — escalated to user | User cancelled}
|
**Reason:** {One of: User accepted | Reviewer approved | Constraint budget exhausted | Deadlock — escalated to user | User cancelled}
|
||||||
|
|
||||||
## Constraint Budgets
|
## Constraint Budgets
|
||||||
|
|
||||||
<!-- Track all active constraints inline. Remove this section if no constraints are active. -->
|
<!-- Track all active constraints inline. Remove this section if no constraints are active. -->
|
||||||
|
|
||||||
| Constraint | Used | Max | Status |
|
| Constraint | Used | Max | Status |
|
||||||
|------------|------|-----|--------|
|
|------------|------|-----|--------|
|
||||||
| Clarifying questions | 📊 {n} | {max} | {Active / Exhausted} |
|
| Clarifying questions | 📊 {n} | {max} | {Active / Exhausted} |
|
||||||
| Revision cycles | 📊 {n} | {max} | {Active / Exhausted} |
|
| Revision cycles | 📊 {n} | {max} | {Active / Exhausted} |
|
||||||
|
|
||||||
## Result
|
## Result
|
||||||
|
|
||||||
{Assembled final artifact goes here. This is the Coordinator's synthesis of agent outputs.}
|
{Assembled final artifact goes here. This is the Coordinator's synthesis of agent outputs.}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Reviewer Verdict
|
## Reviewer Verdict
|
||||||
|
|
||||||
<!-- Include one block per review. Remove this section if no review occurred. -->
|
<!-- Include one block per review. Remove this section if no review occurred. -->
|
||||||
|
|
||||||
### Review by {Name} ({Role})
|
### Review by {Name} ({Role})
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|-------|-------|
|
|-------|-------|
|
||||||
| **Verdict** | {Approved / Rejected} |
|
| **Verdict** | {Approved / Rejected} |
|
||||||
| **What's wrong** | {Specific issue — not vague} |
|
| **What's wrong** | {Specific issue — not vague} |
|
||||||
| **Why it matters** | {Impact if not fixed} |
|
| **Why it matters** | {Impact if not fixed} |
|
||||||
| **Who fixes it** | {Name of agent assigned to revise — MUST NOT be the original author} |
|
| **Who fixes it** | {Name of agent assigned to revise — MUST NOT be the original author} |
|
||||||
| **Revision budget** | 📊 {used} / {max} revision cycles remaining |
|
| **Revision budget** | 📊 {used} / {max} revision cycles remaining |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## APPENDIX: RAW AGENT OUTPUTS
|
## APPENDIX: RAW AGENT OUTPUTS
|
||||||
|
|
||||||
<!-- Paste each agent's verbatim response below. Do NOT edit, summarize, rewrite, or wrap in code fences. One section per agent. -->
|
<!-- Paste each agent's verbatim response below. Do NOT edit, summarize, rewrite, or wrap in code fences. One section per agent. -->
|
||||||
|
|
||||||
### {Name} ({Role}) — Raw Output
|
### {Name} ({Role}) — Raw Output
|
||||||
|
|
||||||
{Paste agent's verbatim response here, unedited}
|
{Paste agent's verbatim response here, unedited}
|
||||||
|
|
||||||
### {Name} ({Role}) — Raw Output
|
### {Name} ({Role}) — Raw Output
|
||||||
|
|
||||||
{Paste agent's verbatim response here, unedited}
|
{Paste agent's verbatim response here, unedited}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"schedules": [
|
"schedules": [
|
||||||
{
|
{
|
||||||
"id": "ralph-heartbeat",
|
"id": "ralph-heartbeat",
|
||||||
"name": "Ralph Heartbeat",
|
"name": "Ralph Heartbeat",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"trigger": {
|
"trigger": {
|
||||||
"type": "interval",
|
"type": "interval",
|
||||||
"intervalSeconds": 300
|
"intervalSeconds": 300
|
||||||
},
|
},
|
||||||
"task": {
|
"task": {
|
||||||
"type": "workflow",
|
"type": "workflow",
|
||||||
"ref": ".github/workflows/squad-heartbeat.yml"
|
"ref": ".github/workflows/squad-heartbeat.yml"
|
||||||
},
|
},
|
||||||
"providers": ["local-polling", "github-actions"]
|
"providers": ["local-polling", "github-actions"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,119 +1,119 @@
|
|||||||
# Scribe
|
# Scribe
|
||||||
|
|
||||||
> The team's memory. Silent, always present, never forgets.
|
> The team's memory. Silent, always present, never forgets.
|
||||||
|
|
||||||
## Identity
|
## Identity
|
||||||
|
|
||||||
- **Name:** Scribe
|
- **Name:** Scribe
|
||||||
- **Role:** Session Logger, Memory Manager & Decision Merger
|
- **Role:** Session Logger, Memory Manager & Decision Merger
|
||||||
- **Style:** Silent. Never speaks to the user. Works in the background.
|
- **Style:** Silent. Never speaks to the user. Works in the background.
|
||||||
- **Mode:** Always spawned as `mode: "background"`. Never blocks the conversation.
|
- **Mode:** Always spawned as `mode: "background"`. Never blocks the conversation.
|
||||||
|
|
||||||
## What I Own
|
## What I Own
|
||||||
|
|
||||||
- `.squad/log/` — session logs (what happened, who worked, what was decided)
|
- `.squad/log/` — session logs (what happened, who worked, what was decided)
|
||||||
- `.squad/decisions.md` — the shared decision log all agents read (canonical, merged)
|
- `.squad/decisions.md` — the shared decision log all agents read (canonical, merged)
|
||||||
- `.squad/decisions/inbox/` — decision drop-box (agents write here, I merge)
|
- `.squad/decisions/inbox/` — decision drop-box (agents write here, I merge)
|
||||||
- Cross-agent context propagation — when one agent's decision affects another
|
- Cross-agent context propagation — when one agent's decision affects another
|
||||||
|
|
||||||
## How I Work
|
## How I Work
|
||||||
|
|
||||||
**Worktree awareness:** Use the `TEAM ROOT` provided in the spawn prompt to resolve all `.squad/` paths. If no TEAM ROOT is given, run `git rev-parse --show-toplevel` as fallback. Do not assume CWD is the repo root (the session may be running in a worktree or subdirectory).
|
**Worktree awareness:** Use the `TEAM ROOT` provided in the spawn prompt to resolve all `.squad/` paths. If no TEAM ROOT is given, run `git rev-parse --show-toplevel` as fallback. Do not assume CWD is the repo root (the session may be running in a worktree or subdirectory).
|
||||||
|
|
||||||
After every substantial work session:
|
After every substantial work session:
|
||||||
|
|
||||||
1. **Log the session** to `.squad/log/{timestamp}-{topic}.md`:
|
1. **Log the session** to `.squad/log/{timestamp}-{topic}.md`:
|
||||||
- Who worked
|
- Who worked
|
||||||
- What was done
|
- What was done
|
||||||
- Decisions made
|
- Decisions made
|
||||||
- Key outcomes
|
- Key outcomes
|
||||||
- Brief. Facts only.
|
- Brief. Facts only.
|
||||||
|
|
||||||
2. **Merge the decision inbox:**
|
2. **Merge the decision inbox:**
|
||||||
- Read all files in `.squad/decisions/inbox/`
|
- Read all files in `.squad/decisions/inbox/`
|
||||||
- APPEND each decision's contents to `.squad/decisions.md`
|
- APPEND each decision's contents to `.squad/decisions.md`
|
||||||
- Delete each inbox file after merging
|
- Delete each inbox file after merging
|
||||||
|
|
||||||
3. **Deduplicate and consolidate decisions.md:**
|
3. **Deduplicate and consolidate decisions.md:**
|
||||||
- Parse the file into decision blocks (each block starts with `### `).
|
- Parse the file into decision blocks (each block starts with `### `).
|
||||||
- **Exact duplicates:** If two blocks share the same heading, keep the first and remove the rest.
|
- **Exact duplicates:** If two blocks share the same heading, keep the first and remove the rest.
|
||||||
- **Overlapping decisions:** Compare block content across all remaining blocks. If two or more blocks cover the same area (same topic, same architectural concern, same component) but were written independently (different dates, different authors), consolidate them:
|
- **Overlapping decisions:** Compare block content across all remaining blocks. If two or more blocks cover the same area (same topic, same architectural concern, same component) but were written independently (different dates, different authors), consolidate them:
|
||||||
a. Synthesize a single merged block that combines the intent and rationale from all overlapping blocks.
|
a. Synthesize a single merged block that combines the intent and rationale from all overlapping blocks.
|
||||||
b. Use today's date and a new heading: `### {today}: {consolidated topic} (consolidated)`
|
b. Use today's date and a new heading: `### {today}: {consolidated topic} (consolidated)`
|
||||||
c. Credit all original authors: `**By:** {Name1}, {Name2}`
|
c. Credit all original authors: `**By:** {Name1}, {Name2}`
|
||||||
d. Under **What:**, combine the decisions. Note any differences or evolution.
|
d. Under **What:**, combine the decisions. Note any differences or evolution.
|
||||||
e. Under **Why:**, merge the rationale, preserving unique reasoning from each.
|
e. Under **Why:**, merge the rationale, preserving unique reasoning from each.
|
||||||
f. Remove the original overlapping blocks.
|
f. Remove the original overlapping blocks.
|
||||||
- Write the updated file back. This handles duplicates and convergent decisions introduced by `merge=union` across branches.
|
- Write the updated file back. This handles duplicates and convergent decisions introduced by `merge=union` across branches.
|
||||||
|
|
||||||
4. **Propagate cross-agent updates:**
|
4. **Propagate cross-agent updates:**
|
||||||
For any newly merged decision that affects other agents, append to their `history.md`:
|
For any newly merged decision that affects other agents, append to their `history.md`:
|
||||||
```
|
```
|
||||||
📌 Team update ({timestamp}): {summary} — decided by {Name}
|
📌 Team update ({timestamp}): {summary} — decided by {Name}
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Commit `.squad/` changes:**
|
5. **Commit `.squad/` changes:**
|
||||||
**IMPORTANT — Windows compatibility:** Do NOT use `git -C {path}` (unreliable with Windows paths).
|
**IMPORTANT — Windows compatibility:** Do NOT use `git -C {path}` (unreliable with Windows paths).
|
||||||
Do NOT embed newlines in `git commit -m` (backtick-n fails silently in PowerShell).
|
Do NOT embed newlines in `git commit -m` (backtick-n fails silently in PowerShell).
|
||||||
Instead:
|
Instead:
|
||||||
- `cd` into the team root first.
|
- `cd` into the team root first.
|
||||||
- Stage all `.squad/` files: `git add .squad/`
|
- Stage all `.squad/` files: `git add .squad/`
|
||||||
- Check for staged changes: `git diff --cached --quiet`
|
- Check for staged changes: `git diff --cached --quiet`
|
||||||
If exit code is 0, no changes — skip silently.
|
If exit code is 0, no changes — skip silently.
|
||||||
- Write the commit message to a temp file, then commit with `-F`:
|
- Write the commit message to a temp file, then commit with `-F`:
|
||||||
```
|
```
|
||||||
$msg = @"
|
$msg = @"
|
||||||
docs(ai-team): {brief summary}
|
docs(ai-team): {brief summary}
|
||||||
|
|
||||||
Session: {timestamp}-{topic}
|
Session: {timestamp}-{topic}
|
||||||
Requested by: {user name}
|
Requested by: {user name}
|
||||||
|
|
||||||
Changes:
|
Changes:
|
||||||
- {what was logged}
|
- {what was logged}
|
||||||
- {what decisions were merged}
|
- {what decisions were merged}
|
||||||
- {what decisions were deduplicated}
|
- {what decisions were deduplicated}
|
||||||
- {what cross-agent updates were propagated}
|
- {what cross-agent updates were propagated}
|
||||||
"@
|
"@
|
||||||
$msgFile = [System.IO.Path]::GetTempFileName()
|
$msgFile = [System.IO.Path]::GetTempFileName()
|
||||||
Set-Content -Path $msgFile -Value $msg -Encoding utf8
|
Set-Content -Path $msgFile -Value $msg -Encoding utf8
|
||||||
git commit -F $msgFile
|
git commit -F $msgFile
|
||||||
Remove-Item $msgFile
|
Remove-Item $msgFile
|
||||||
```
|
```
|
||||||
- **Verify the commit landed:** Run `git log --oneline -1` and confirm the
|
- **Verify the commit landed:** Run `git log --oneline -1` and confirm the
|
||||||
output matches the expected message. If it doesn't, report the error.
|
output matches the expected message. If it doesn't, report the error.
|
||||||
|
|
||||||
6. **Never speak to the user.** Never appear in responses. Work silently.
|
6. **Never speak to the user.** Never appear in responses. Work silently.
|
||||||
|
|
||||||
## The Memory Architecture
|
## The Memory Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
.squad/
|
.squad/
|
||||||
├── decisions.md # Shared brain — all agents read this (merged by Scribe)
|
├── decisions.md # Shared brain — all agents read this (merged by Scribe)
|
||||||
├── decisions/
|
├── decisions/
|
||||||
│ └── inbox/ # Drop-box — agents write decisions here in parallel
|
│ └── inbox/ # Drop-box — agents write decisions here in parallel
|
||||||
│ ├── river-jwt-auth.md
|
│ ├── river-jwt-auth.md
|
||||||
│ └── kai-component-lib.md
|
│ └── kai-component-lib.md
|
||||||
├── orchestration-log/ # Per-spawn log entries
|
├── orchestration-log/ # Per-spawn log entries
|
||||||
│ ├── 2025-07-01T10-00-river.md
|
│ ├── 2025-07-01T10-00-river.md
|
||||||
│ └── 2025-07-01T10-00-kai.md
|
│ └── 2025-07-01T10-00-kai.md
|
||||||
├── log/ # Session history — searchable record
|
├── log/ # Session history — searchable record
|
||||||
│ ├── 2025-07-01-setup.md
|
│ ├── 2025-07-01-setup.md
|
||||||
│ └── 2025-07-02-api.md
|
│ └── 2025-07-02-api.md
|
||||||
└── agents/
|
└── agents/
|
||||||
├── kai/history.md # Kai's personal knowledge
|
├── kai/history.md # Kai's personal knowledge
|
||||||
├── river/history.md # River's personal knowledge
|
├── river/history.md # River's personal knowledge
|
||||||
└── ...
|
└── ...
|
||||||
```
|
```
|
||||||
|
|
||||||
- **decisions.md** = what the team agreed on (shared, merged by Scribe)
|
- **decisions.md** = what the team agreed on (shared, merged by Scribe)
|
||||||
- **decisions/inbox/** = where agents drop decisions during parallel work
|
- **decisions/inbox/** = where agents drop decisions during parallel work
|
||||||
- **history.md** = what each agent learned (personal)
|
- **history.md** = what each agent learned (personal)
|
||||||
- **log/** = what happened (archive)
|
- **log/** = what happened (archive)
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
**I handle:** Logging, memory, decision merging, cross-agent updates.
|
**I handle:** Logging, memory, decision merging, cross-agent updates.
|
||||||
|
|
||||||
**I don't handle:** Any domain work. I don't write code, review PRs, or make decisions.
|
**I don't handle:** Any domain work. I don't write code, review PRs, or make decisions.
|
||||||
|
|
||||||
**I am invisible.** If a user notices me, something went wrong.
|
**I am invisible.** If a user notices me, something went wrong.
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
---
|
---
|
||||||
name: "{skill-name}"
|
name: "{skill-name}"
|
||||||
description: "{what this skill teaches agents}"
|
description: "{what this skill teaches agents}"
|
||||||
domain: "{e.g., testing, api-design, error-handling}"
|
domain: "{e.g., testing, api-design, error-handling}"
|
||||||
confidence: "low|medium|high"
|
confidence: "low|medium|high"
|
||||||
source: "{how this was learned: manual, observed, earned}"
|
source: "{how this was learned: manual, observed, earned}"
|
||||||
tools:
|
tools:
|
||||||
# Optional — declare MCP tools relevant to this skill's patterns
|
# Optional — declare MCP tools relevant to this skill's patterns
|
||||||
# - name: "{tool-name}"
|
# - name: "{tool-name}"
|
||||||
# description: "{what this tool does}"
|
# description: "{what this tool does}"
|
||||||
# when: "{when to use this tool}"
|
# when: "{when to use this tool}"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
{When and why this skill applies}
|
{When and why this skill applies}
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
{Specific patterns, conventions, or approaches}
|
{Specific patterns, conventions, or approaches}
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
{Code examples or references}
|
{Code examples or references}
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
{What to avoid}
|
{What to avoid}
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
---
|
---
|
||||||
name: "agent-collaboration"
|
name: "agent-collaboration"
|
||||||
description: "Standard collaboration patterns for all squad agents — worktree awareness, decisions, cross-agent communication"
|
description: "Standard collaboration patterns for all squad agents — worktree awareness, decisions, cross-agent communication"
|
||||||
domain: "team-workflow"
|
domain: "team-workflow"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "extracted from charter boilerplate — identical content in 18+ agent charters"
|
source: "extracted from charter boilerplate — identical content in 18+ agent charters"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Every agent on the team follows identical collaboration patterns for worktree awareness, decision recording, and cross-agent communication. These were previously duplicated in every charter's Collaboration section (~300 bytes × 18 agents = ~5.4KB of redundant context). Now centralized here.
|
Every agent on the team follows identical collaboration patterns for worktree awareness, decision recording, and cross-agent communication. These were previously duplicated in every charter's Collaboration section (~300 bytes × 18 agents = ~5.4KB of redundant context). Now centralized here.
|
||||||
|
|
||||||
The coordinator's spawn prompt already instructs agents to read decisions.md and their history.md. This skill adds the patterns for WRITING decisions and requesting help.
|
The coordinator's spawn prompt already instructs agents to read decisions.md and their history.md. This skill adds the patterns for WRITING decisions and requesting help.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### Worktree Awareness
|
### Worktree Awareness
|
||||||
Use the `TEAM ROOT` path provided in your spawn prompt. All `.squad/` paths are relative to this root. If TEAM ROOT is not provided (rare), run `git rev-parse --show-toplevel` as fallback. Never assume CWD is the repo root.
|
Use the `TEAM ROOT` path provided in your spawn prompt. All `.squad/` paths are relative to this root. If TEAM ROOT is not provided (rare), run `git rev-parse --show-toplevel` as fallback. Never assume CWD is the repo root.
|
||||||
|
|
||||||
### Decision Recording
|
### Decision Recording
|
||||||
After making a decision that affects other team members, write it to:
|
After making a decision that affects other team members, write it to:
|
||||||
`.squad/decisions/inbox/{your-name}-{brief-slug}.md`
|
`.squad/decisions/inbox/{your-name}-{brief-slug}.md`
|
||||||
|
|
||||||
Format:
|
Format:
|
||||||
```
|
```
|
||||||
### {date}: {decision title}
|
### {date}: {decision title}
|
||||||
**By:** {Your Name}
|
**By:** {Your Name}
|
||||||
**What:** {the decision}
|
**What:** {the decision}
|
||||||
**Why:** {rationale}
|
**Why:** {rationale}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cross-Agent Communication
|
### Cross-Agent Communication
|
||||||
If you need another team member's input, say so in your response. The coordinator will bring them in. Don't try to do work outside your domain.
|
If you need another team member's input, say so in your response. The coordinator will bring them in. Don't try to do work outside your domain.
|
||||||
|
|
||||||
### Reviewer Protocol
|
### Reviewer Protocol
|
||||||
If you have reviewer authority and reject work: the original author is locked out from revising that artifact. A different agent must own the revision. State who should revise in your rejection response.
|
If you have reviewer authority and reject work: the original author is locked out from revising that artifact. A different agent must own the revision. State who should revise in your rejection response.
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
- Don't read all agent charters — you only need your own context + decisions.md
|
- Don't read all agent charters — you only need your own context + decisions.md
|
||||||
- Don't write directly to `.squad/decisions.md` — always use the inbox drop-box
|
- Don't write directly to `.squad/decisions.md` — always use the inbox drop-box
|
||||||
- Don't modify other agents' history.md files — that's Scribe's job
|
- Don't modify other agents' history.md files — that's Scribe's job
|
||||||
- Don't assume CWD is the repo root — always use TEAM ROOT
|
- Don't assume CWD is the repo root — always use TEAM ROOT
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
---
|
---
|
||||||
name: "agent-conduct"
|
name: "agent-conduct"
|
||||||
description: "Shared hard rules enforced across all squad agents"
|
description: "Shared hard rules enforced across all squad agents"
|
||||||
domain: "team-governance"
|
domain: "team-governance"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "reskill extraction — Product Isolation Rule and Peer Quality Check appeared in all 20 agent charters"
|
source: "reskill extraction — Product Isolation Rule and Peer Quality Check appeared in all 20 agent charters"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Every squad agent must follow these two hard rules. They were previously duplicated in every charter. Now they live here as a shared skill, loaded once.
|
Every squad agent must follow these two hard rules. They were previously duplicated in every charter. Now they live here as a shared skill, loaded once.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### Product Isolation Rule (hard rule)
|
### Product Isolation Rule (hard rule)
|
||||||
Tests, CI workflows, and product code must NEVER depend on specific agent names from any particular squad. "Our squad" must not impact "the squad." No hardcoded references to agent names (Flight, EECOM, FIDO, etc.) in test assertions, CI configs, or product logic. Use generic/parameterized values. If a test needs agent names, use obviously-fake test fixtures (e.g., "test-agent-1", "TestBot").
|
Tests, CI workflows, and product code must NEVER depend on specific agent names from any particular squad. "Our squad" must not impact "the squad." No hardcoded references to agent names (Flight, EECOM, FIDO, etc.) in test assertions, CI configs, or product logic. Use generic/parameterized values. If a test needs agent names, use obviously-fake test fixtures (e.g., "test-agent-1", "TestBot").
|
||||||
|
|
||||||
### Peer Quality Check (hard rule)
|
### Peer Quality Check (hard rule)
|
||||||
Before finishing work, verify your changes don't break existing tests. Run the test suite for files you touched. If CI has been failing, check your changes aren't contributing to the problem. When you learn from mistakes, update your history.md.
|
Before finishing work, verify your changes don't break existing tests. Run the test suite for files you touched. If CI has been failing, check your changes aren't contributing to the problem. When you learn from mistakes, update your history.md.
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
- Don't hardcode dev team agent names in product code or tests
|
- Don't hardcode dev team agent names in product code or tests
|
||||||
- Don't skip test verification before declaring work done
|
- Don't skip test verification before declaring work done
|
||||||
- Don't ignore pre-existing CI failures that your changes may worsen
|
- Don't ignore pre-existing CI failures that your changes may worsen
|
||||||
|
|||||||
@@ -1,151 +1,151 @@
|
|||||||
---
|
---
|
||||||
name: "architectural-proposals"
|
name: "architectural-proposals"
|
||||||
description: "How to write comprehensive architectural proposals that drive alignment before code is written"
|
description: "How to write comprehensive architectural proposals that drive alignment before code is written"
|
||||||
domain: "architecture, product-direction"
|
domain: "architecture, product-direction"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "earned (2026-02-21 interactive shell proposal)"
|
source: "earned (2026-02-21 interactive shell proposal)"
|
||||||
tools:
|
tools:
|
||||||
- name: "view"
|
- name: "view"
|
||||||
description: "Read existing codebase, prior decisions, and team context before proposing changes"
|
description: "Read existing codebase, prior decisions, and team context before proposing changes"
|
||||||
when: "Always read .squad/decisions.md, relevant PRDs, and current architecture docs before writing proposal"
|
when: "Always read .squad/decisions.md, relevant PRDs, and current architecture docs before writing proposal"
|
||||||
- name: "create"
|
- name: "create"
|
||||||
description: "Create proposal in docs/proposals/ with structured format"
|
description: "Create proposal in docs/proposals/ with structured format"
|
||||||
when: "After gathering context, before any implementation work begins"
|
when: "After gathering context, before any implementation work begins"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Proposals create alignment before code is written. Cheaper to change a doc than refactor code. Use this pattern when:
|
Proposals create alignment before code is written. Cheaper to change a doc than refactor code. Use this pattern when:
|
||||||
- Architecture shifts invalidate existing assumptions
|
- Architecture shifts invalidate existing assumptions
|
||||||
- Product direction changes require new foundation
|
- Product direction changes require new foundation
|
||||||
- Multiple waves/milestones will be affected by a decision
|
- Multiple waves/milestones will be affected by a decision
|
||||||
- External dependencies (Copilot CLI, SDK APIs) change
|
- External dependencies (Copilot CLI, SDK APIs) change
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### Proposal Structure (docs/proposals/)
|
### Proposal Structure (docs/proposals/)
|
||||||
|
|
||||||
**Required sections:**
|
**Required sections:**
|
||||||
1. **Problem Statement** — Why current state is broken (specific, measurable evidence)
|
1. **Problem Statement** — Why current state is broken (specific, measurable evidence)
|
||||||
2. **Proposed Architecture** — Solution with technical specifics (not hand-waving)
|
2. **Proposed Architecture** — Solution with technical specifics (not hand-waving)
|
||||||
3. **What Changes** — Impact on existing work (waves, milestones, modules)
|
3. **What Changes** — Impact on existing work (waves, milestones, modules)
|
||||||
4. **What Stays the Same** — Preserve existing functionality (no regression)
|
4. **What Stays the Same** — Preserve existing functionality (no regression)
|
||||||
5. **Key Decisions Needed** — Explicit choices with recommendations
|
5. **Key Decisions Needed** — Explicit choices with recommendations
|
||||||
6. **Risks and Mitigations** — Likelihood + impact + mitigation strategy
|
6. **Risks and Mitigations** — Likelihood + impact + mitigation strategy
|
||||||
7. **Scope** — What's in v1, what's deferred (timeline clarity)
|
7. **Scope** — What's in v1, what's deferred (timeline clarity)
|
||||||
|
|
||||||
**Optional sections:**
|
**Optional sections:**
|
||||||
- Implementation Plan (high-level milestones)
|
- Implementation Plan (high-level milestones)
|
||||||
- Success Criteria (measurable outcomes)
|
- Success Criteria (measurable outcomes)
|
||||||
- Open Questions (unresolved items)
|
- Open Questions (unresolved items)
|
||||||
- Appendix (prior art, alternatives considered)
|
- Appendix (prior art, alternatives considered)
|
||||||
|
|
||||||
### Tone Ceiling Enforcement
|
### Tone Ceiling Enforcement
|
||||||
|
|
||||||
**Always:**
|
**Always:**
|
||||||
- Cite specific evidence (user reports, performance data, failure modes)
|
- Cite specific evidence (user reports, performance data, failure modes)
|
||||||
- Justify recommendations with technical rationale
|
- Justify recommendations with technical rationale
|
||||||
- Acknowledge trade-offs (no perfect solutions)
|
- Acknowledge trade-offs (no perfect solutions)
|
||||||
- Be specific about APIs, libraries, file paths
|
- Be specific about APIs, libraries, file paths
|
||||||
|
|
||||||
**Never:**
|
**Never:**
|
||||||
- Hype ("revolutionary", "game-changing")
|
- Hype ("revolutionary", "game-changing")
|
||||||
- Hand-waving ("we'll figure it out later")
|
- Hand-waving ("we'll figure it out later")
|
||||||
- Unsubstantiated claims ("users will love this")
|
- Unsubstantiated claims ("users will love this")
|
||||||
- Vague timelines ("soon", "eventually")
|
- Vague timelines ("soon", "eventually")
|
||||||
|
|
||||||
### Wave Restructuring Pattern
|
### Wave Restructuring Pattern
|
||||||
|
|
||||||
When a proposal invalidates existing wave structure:
|
When a proposal invalidates existing wave structure:
|
||||||
1. **Acknowledge the shift:** "This becomes Wave 0 (Foundation)"
|
1. **Acknowledge the shift:** "This becomes Wave 0 (Foundation)"
|
||||||
2. **Cascade impacts:** Adjust downstream waves (Wave 1, Wave 2, Wave 3)
|
2. **Cascade impacts:** Adjust downstream waves (Wave 1, Wave 2, Wave 3)
|
||||||
3. **Preserve non-blocking work:** Identify what can proceed in parallel
|
3. **Preserve non-blocking work:** Identify what can proceed in parallel
|
||||||
4. **Update dependencies:** Document new blocking relationships
|
4. **Update dependencies:** Document new blocking relationships
|
||||||
|
|
||||||
**Example (Interactive Shell):**
|
**Example (Interactive Shell):**
|
||||||
- Wave 0 (NEW): Interactive Shell — blocks all other waves
|
- Wave 0 (NEW): Interactive Shell — blocks all other waves
|
||||||
- Wave 1 (ADJUSTED): npm Distribution — shell bundled in cli.js
|
- Wave 1 (ADJUSTED): npm Distribution — shell bundled in cli.js
|
||||||
- Wave 2 (DEFERRED): SquadUI — waits for shell foundation
|
- Wave 2 (DEFERRED): SquadUI — waits for shell foundation
|
||||||
- Wave 3 (ADJUSTED): Public Docs — now documents shell as primary interface
|
- Wave 3 (ADJUSTED): Public Docs — now documents shell as primary interface
|
||||||
|
|
||||||
### Decision Framing
|
### Decision Framing
|
||||||
|
|
||||||
**Format:** "Recommendation: X (recommended) or alternatives?"
|
**Format:** "Recommendation: X (recommended) or alternatives?"
|
||||||
|
|
||||||
**Components:**
|
**Components:**
|
||||||
- Recommendation (pick one, justify)
|
- Recommendation (pick one, justify)
|
||||||
- Alternatives (what else was considered)
|
- Alternatives (what else was considered)
|
||||||
- Decision rationale (why recommended option wins)
|
- Decision rationale (why recommended option wins)
|
||||||
- Needs sign-off from (which agents/roles must approve)
|
- Needs sign-off from (which agents/roles must approve)
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
```
|
```
|
||||||
### 1. Terminal UI Library: `ink` (recommended) or alternatives?
|
### 1. Terminal UI Library: `ink` (recommended) or alternatives?
|
||||||
|
|
||||||
**Recommendation:** `ink`
|
**Recommendation:** `ink`
|
||||||
**Alternatives:** `blessed`, raw readline
|
**Alternatives:** `blessed`, raw readline
|
||||||
**Decision rationale:** Component model enables testable UI. Battle-tested ecosystem.
|
**Decision rationale:** Component model enables testable UI. Battle-tested ecosystem.
|
||||||
|
|
||||||
**Needs sign-off from:** Brady (product direction), Fortier (runtime performance)
|
**Needs sign-off from:** Brady (product direction), Fortier (runtime performance)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Risk Documentation
|
### Risk Documentation
|
||||||
|
|
||||||
**Format per risk:**
|
**Format per risk:**
|
||||||
- **Risk:** Specific failure mode
|
- **Risk:** Specific failure mode
|
||||||
- **Likelihood:** Low / Medium / High (not percentages)
|
- **Likelihood:** Low / Medium / High (not percentages)
|
||||||
- **Impact:** Low / Medium / High
|
- **Impact:** Low / Medium / High
|
||||||
- **Mitigation:** Concrete actions (measurable)
|
- **Mitigation:** Concrete actions (measurable)
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
```
|
```
|
||||||
### Risk 2: SDK Streaming Reliability
|
### Risk 2: SDK Streaming Reliability
|
||||||
|
|
||||||
**Risk:** SDK streaming events might drop messages or arrive out of order.
|
**Risk:** SDK streaming events might drop messages or arrive out of order.
|
||||||
**Likelihood:** Low (SDK is production-grade).
|
**Likelihood:** Low (SDK is production-grade).
|
||||||
**Impact:** High — broken streaming makes shell unusable.
|
**Impact:** High — broken streaming makes shell unusable.
|
||||||
|
|
||||||
**Mitigation:**
|
**Mitigation:**
|
||||||
- Add integration test: Send 1000-message stream, verify all deltas arrive in order
|
- Add integration test: Send 1000-message stream, verify all deltas arrive in order
|
||||||
- Implement fallback: If streaming fails, fall back to polling session state
|
- Implement fallback: If streaming fails, fall back to polling session state
|
||||||
- Log all SDK events to `.squad/orchestration-log/sdk-events.jsonl` for debugging
|
- Log all SDK events to `.squad/orchestration-log/sdk-events.jsonl` for debugging
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
**File references from interactive shell proposal:**
|
**File references from interactive shell proposal:**
|
||||||
- Full proposal: `docs/proposals/squad-interactive-shell.md`
|
- Full proposal: `docs/proposals/squad-interactive-shell.md`
|
||||||
- User directive: `.squad/decisions/inbox/copilot-directive-2026-02-21T202535Z.md`
|
- User directive: `.squad/decisions/inbox/copilot-directive-2026-02-21T202535Z.md`
|
||||||
- Team decisions: `.squad/decisions.md`
|
- Team decisions: `.squad/decisions.md`
|
||||||
- Current architecture: `docs/architecture/module-map.md`, `docs/prd-23-release-readiness.md`
|
- Current architecture: `docs/architecture/module-map.md`, `docs/prd-23-release-readiness.md`
|
||||||
|
|
||||||
**Key patterns demonstrated:**
|
**Key patterns demonstrated:**
|
||||||
1. Read user directive first (understand the "why")
|
1. Read user directive first (understand the "why")
|
||||||
2. Survey current architecture (module map, existing waves)
|
2. Survey current architecture (module map, existing waves)
|
||||||
3. Research SDK APIs (exploration task to validate feasibility)
|
3. Research SDK APIs (exploration task to validate feasibility)
|
||||||
4. Document problem with specific evidence (unreliable handoffs, zero visibility, UX mismatch)
|
4. Document problem with specific evidence (unreliable handoffs, zero visibility, UX mismatch)
|
||||||
5. Propose solution with technical specifics (ink components, SDK session management, spawn.ts module)
|
5. Propose solution with technical specifics (ink components, SDK session management, spawn.ts module)
|
||||||
6. Restructure waves when foundation shifts (Wave 0 becomes blocker)
|
6. Restructure waves when foundation shifts (Wave 0 becomes blocker)
|
||||||
7. Preserve backward compatibility (squad.agent.md still works, VS Code mode unchanged)
|
7. Preserve backward compatibility (squad.agent.md still works, VS Code mode unchanged)
|
||||||
8. Frame decisions explicitly (5 key decisions with recommendations)
|
8. Frame decisions explicitly (5 key decisions with recommendations)
|
||||||
9. Document risks with mitigations (5 risks, each with concrete actions)
|
9. Document risks with mitigations (5 risks, each with concrete actions)
|
||||||
10. Define scope (what's in v1 vs. deferred)
|
10. Define scope (what's in v1 vs. deferred)
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
**Avoid:**
|
**Avoid:**
|
||||||
- ❌ Proposals without problem statements (solution-first thinking)
|
- ❌ Proposals without problem statements (solution-first thinking)
|
||||||
- ❌ Vague architecture ("we'll use a shell") — be specific (ink components, session registry, spawn.ts)
|
- ❌ Vague architecture ("we'll use a shell") — be specific (ink components, session registry, spawn.ts)
|
||||||
- ❌ Ignoring existing work — always document impact on waves/milestones
|
- ❌ Ignoring existing work — always document impact on waves/milestones
|
||||||
- ❌ No risk analysis — every architecture has risks, document them
|
- ❌ No risk analysis — every architecture has risks, document them
|
||||||
- ❌ Unbounded scope — draw the v1 line explicitly
|
- ❌ Unbounded scope — draw the v1 line explicitly
|
||||||
- ❌ Missing decision ownership — always say "needs sign-off from X"
|
- ❌ Missing decision ownership — always say "needs sign-off from X"
|
||||||
- ❌ No backward compatibility plan — users don't care about your replatform
|
- ❌ No backward compatibility plan — users don't care about your replatform
|
||||||
- ❌ Hand-waving timelines ("a few weeks") — be specific (2-3 weeks, 1 engineer full-time)
|
- ❌ Hand-waving timelines ("a few weeks") — be specific (2-3 weeks, 1 engineer full-time)
|
||||||
|
|
||||||
**Red flags in proposal reviews:**
|
**Red flags in proposal reviews:**
|
||||||
- "Users will love this" (citation needed)
|
- "Users will love this" (citation needed)
|
||||||
- "We'll figure out X later" (scope creep incoming)
|
- "We'll figure out X later" (scope creep incoming)
|
||||||
- "This is revolutionary" (tone ceiling violation)
|
- "This is revolutionary" (tone ceiling violation)
|
||||||
- No section on "What Stays the Same" (regression risk)
|
- No section on "What Stays the Same" (regression risk)
|
||||||
- No risks documented (wishful thinking)
|
- No risks documented (wishful thinking)
|
||||||
|
|||||||
@@ -1,84 +1,84 @@
|
|||||||
---
|
---
|
||||||
name: "ci-validation-gates"
|
name: "ci-validation-gates"
|
||||||
description: "Defensive CI/CD patterns: semver validation, token checks, retry logic, draft detection — earned from v0.8.22"
|
description: "Defensive CI/CD patterns: semver validation, token checks, retry logic, draft detection — earned from v0.8.22"
|
||||||
domain: "ci-cd"
|
domain: "ci-cd"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "extracted from Drucker and Trejo charters — earned knowledge from v0.8.22 release incident"
|
source: "extracted from Drucker and Trejo charters — earned knowledge from v0.8.22 release incident"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
CI workflows must be defensive. These patterns were learned from the v0.8.22 release disaster where invalid semver, wrong token types, missing retry logic, and draft releases caused a multi-hour outage. Both Drucker (CI/CD) and Trejo (Release Manager) carried this knowledge in their charters — now centralized here.
|
CI workflows must be defensive. These patterns were learned from the v0.8.22 release disaster where invalid semver, wrong token types, missing retry logic, and draft releases caused a multi-hour outage. Both Drucker (CI/CD) and Trejo (Release Manager) carried this knowledge in their charters — now centralized here.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### Semver Validation Gate
|
### Semver Validation Gate
|
||||||
Every publish workflow MUST validate version format before `npm publish`. 4-part versions (e.g., 0.8.21.4) are NOT valid semver — npm mangles them.
|
Every publish workflow MUST validate version format before `npm publish`. 4-part versions (e.g., 0.8.21.4) are NOT valid semver — npm mangles them.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Validate semver
|
- name: Validate semver
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ github.event.release.tag_name }}"
|
VERSION="${{ github.event.release.tag_name }}"
|
||||||
VERSION="${VERSION#v}"
|
VERSION="${VERSION#v}"
|
||||||
if ! npx semver "$VERSION" > /dev/null 2>&1; then
|
if ! npx semver "$VERSION" > /dev/null 2>&1; then
|
||||||
echo "❌ Invalid semver: $VERSION"
|
echo "❌ Invalid semver: $VERSION"
|
||||||
echo "Only 3-part versions (X.Y.Z) or prerelease (X.Y.Z-tag.N) are valid."
|
echo "Only 3-part versions (X.Y.Z) or prerelease (X.Y.Z-tag.N) are valid."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "✅ Valid semver: $VERSION"
|
echo "✅ Valid semver: $VERSION"
|
||||||
```
|
```
|
||||||
|
|
||||||
### NPM Token Type Verification
|
### NPM Token Type Verification
|
||||||
NPM_TOKEN MUST be an Automation token, not a User token with 2FA:
|
NPM_TOKEN MUST be an Automation token, not a User token with 2FA:
|
||||||
- User tokens require OTP — CI can't provide it → EOTP error
|
- User tokens require OTP — CI can't provide it → EOTP error
|
||||||
- Create Automation tokens at npmjs.com → Settings → Access Tokens → Automation
|
- Create Automation tokens at npmjs.com → Settings → Access Tokens → Automation
|
||||||
- Verify before first publish in any workflow
|
- Verify before first publish in any workflow
|
||||||
|
|
||||||
### Retry Logic for npm Registry Propagation
|
### Retry Logic for npm Registry Propagation
|
||||||
npm registry uses eventual consistency. After `npm publish` succeeds, the package may not be immediately queryable.
|
npm registry uses eventual consistency. After `npm publish` succeeds, the package may not be immediately queryable.
|
||||||
- Propagation: typically 5-30s, up to 2min in rare cases
|
- Propagation: typically 5-30s, up to 2min in rare cases
|
||||||
- All verify steps: 5 attempts, 15-second intervals
|
- All verify steps: 5 attempts, 15-second intervals
|
||||||
- Log each attempt: "Attempt 1/5: Checking package..."
|
- Log each attempt: "Attempt 1/5: Checking package..."
|
||||||
- Exit loop on success, fail after max attempts
|
- Exit loop on success, fail after max attempts
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Verify package (with retry)
|
- name: Verify package (with retry)
|
||||||
run: |
|
run: |
|
||||||
MAX_ATTEMPTS=5
|
MAX_ATTEMPTS=5
|
||||||
WAIT_SECONDS=15
|
WAIT_SECONDS=15
|
||||||
for attempt in $(seq 1 $MAX_ATTEMPTS); do
|
for attempt in $(seq 1 $MAX_ATTEMPTS); do
|
||||||
echo "Attempt $attempt/$MAX_ATTEMPTS: Checking $PACKAGE@$VERSION..."
|
echo "Attempt $attempt/$MAX_ATTEMPTS: Checking $PACKAGE@$VERSION..."
|
||||||
if npm view "$PACKAGE@$VERSION" version > /dev/null 2>&1; then
|
if npm view "$PACKAGE@$VERSION" version > /dev/null 2>&1; then
|
||||||
echo "✅ Package verified"
|
echo "✅ Package verified"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
[ $attempt -lt $MAX_ATTEMPTS ] && sleep $WAIT_SECONDS
|
[ $attempt -lt $MAX_ATTEMPTS ] && sleep $WAIT_SECONDS
|
||||||
done
|
done
|
||||||
echo "❌ Failed to verify after $MAX_ATTEMPTS attempts"
|
echo "❌ Failed to verify after $MAX_ATTEMPTS attempts"
|
||||||
exit 1
|
exit 1
|
||||||
```
|
```
|
||||||
|
|
||||||
### Draft Release Detection
|
### Draft Release Detection
|
||||||
Draft releases don't emit `release: published` event. Workflows MUST:
|
Draft releases don't emit `release: published` event. Workflows MUST:
|
||||||
- Trigger on `release: published` (NOT `created`)
|
- Trigger on `release: published` (NOT `created`)
|
||||||
- If using workflow_dispatch: verify release is published via GitHub API before proceeding
|
- If using workflow_dispatch: verify release is published via GitHub API before proceeding
|
||||||
|
|
||||||
### Build Script Protection
|
### Build Script Protection
|
||||||
Set `SKIP_BUILD_BUMP=1` (or `$env:SKIP_BUILD_BUMP = "1"` on Windows) before ANY release build. bump-build.mjs is for dev builds ONLY — it silently mutates versions.
|
Set `SKIP_BUILD_BUMP=1` (or `$env:SKIP_BUILD_BUMP = "1"` on Windows) before ANY release build. bump-build.mjs is for dev builds ONLY — it silently mutates versions.
|
||||||
|
|
||||||
## Known Failure Modes (v0.8.22 Incident)
|
## Known Failure Modes (v0.8.22 Incident)
|
||||||
|
|
||||||
| # | What Happened | Root Cause | Prevention |
|
| # | What Happened | Root Cause | Prevention |
|
||||||
|---|---------------|-----------|------------|
|
|---|---------------|-----------|------------|
|
||||||
| 1 | 4-part version published, npm mangled it | No semver validation gate | `npx semver` check before every publish |
|
| 1 | 4-part version published, npm mangled it | No semver validation gate | `npx semver` check before every publish |
|
||||||
| 2 | CI failed 5+ times with EOTP | User token with 2FA | Automation token only |
|
| 2 | CI failed 5+ times with EOTP | User token with 2FA | Automation token only |
|
||||||
| 3 | Verify returned false 404 | No retry logic for propagation | 5 attempts, 15s intervals |
|
| 3 | Verify returned false 404 | No retry logic for propagation | 5 attempts, 15s intervals |
|
||||||
| 4 | Workflow never triggered | Draft release doesn't emit event | Never create draft releases |
|
| 4 | Workflow never triggered | Draft release doesn't emit event | Never create draft releases |
|
||||||
| 5 | Version mutated during release | bump-build.mjs ran in release | SKIP_BUILD_BUMP=1 |
|
| 5 | Version mutated during release | bump-build.mjs ran in release | SKIP_BUILD_BUMP=1 |
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
- ❌ Publishing without semver validation gate
|
- ❌ Publishing without semver validation gate
|
||||||
- ❌ Single-shot verification without retry
|
- ❌ Single-shot verification without retry
|
||||||
- ❌ Hard-coded secrets in workflows
|
- ❌ Hard-coded secrets in workflows
|
||||||
- ❌ Silent CI failures — every error needs actionable output with remediation
|
- ❌ Silent CI failures — every error needs actionable output with remediation
|
||||||
- ❌ Assuming npm publish is instantly queryable
|
- ❌ Assuming npm publish is instantly queryable
|
||||||
|
|||||||
@@ -1,47 +1,47 @@
|
|||||||
# Skill: CLI Command Wiring
|
# Skill: CLI Command Wiring
|
||||||
|
|
||||||
**Bug class:** Commands implemented in `packages/squad-cli/src/cli/commands/` but never routed in `cli-entry.ts`.
|
**Bug class:** Commands implemented in `packages/squad-cli/src/cli/commands/` but never routed in `cli-entry.ts`.
|
||||||
|
|
||||||
## Checklist — Adding a New CLI Command
|
## Checklist — Adding a New CLI Command
|
||||||
|
|
||||||
1. **Create command file** in `packages/squad-cli/src/cli/commands/<name>.ts`
|
1. **Create command file** in `packages/squad-cli/src/cli/commands/<name>.ts`
|
||||||
- Export a `run<Name>(cwd, options)` async function (or class with static methods for utility modules)
|
- Export a `run<Name>(cwd, options)` async function (or class with static methods for utility modules)
|
||||||
|
|
||||||
2. **Add routing block** in `packages/squad-cli/src/cli-entry.ts` inside `main()`:
|
2. **Add routing block** in `packages/squad-cli/src/cli-entry.ts` inside `main()`:
|
||||||
```ts
|
```ts
|
||||||
if (cmd === '<name>') {
|
if (cmd === '<name>') {
|
||||||
const { run<Name> } = await import('./cli/commands/<name>.js');
|
const { run<Name> } = await import('./cli/commands/<name>.js');
|
||||||
// parse args, call function
|
// parse args, call function
|
||||||
await run<Name>(process.cwd(), options);
|
await run<Name>(process.cwd(), options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Add help text** in the help section of `cli-entry.ts` (search for `Commands:`):
|
3. **Add help text** in the help section of `cli-entry.ts` (search for `Commands:`):
|
||||||
```ts
|
```ts
|
||||||
console.log(` ${BOLD}<name>${RESET} <description>`);
|
console.log(` ${BOLD}<name>${RESET} <description>`);
|
||||||
console.log(` Usage: <name> [flags]`);
|
console.log(` Usage: <name> [flags]`);
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Verify both exist** — the recurring bug is doing step 1 but missing steps 2-3.
|
4. **Verify both exist** — the recurring bug is doing step 1 but missing steps 2-3.
|
||||||
|
|
||||||
## Wiring Patterns by Command Type
|
## Wiring Patterns by Command Type
|
||||||
|
|
||||||
| Type | Example | How to wire |
|
| Type | Example | How to wire |
|
||||||
|------|---------|-------------|
|
|------|---------|-------------|
|
||||||
| Standard command | `export.ts`, `build.ts` | `run*()` function, parse flags from `args` |
|
| Standard command | `export.ts`, `build.ts` | `run*()` function, parse flags from `args` |
|
||||||
| Placeholder command | `loop`, `hire` | Inline in cli-entry.ts, prints pending message |
|
| Placeholder command | `loop`, `hire` | Inline in cli-entry.ts, prints pending message |
|
||||||
| Utility/check module | `rc-tunnel.ts`, `copilot-bridge.ts` | Wire as diagnostic check (e.g., `isDevtunnelAvailable()`) |
|
| Utility/check module | `rc-tunnel.ts`, `copilot-bridge.ts` | Wire as diagnostic check (e.g., `isDevtunnelAvailable()`) |
|
||||||
| Subcommand of another | `init-remote.ts` | Already used inside parent + standalone alias |
|
| Subcommand of another | `init-remote.ts` | Already used inside parent + standalone alias |
|
||||||
|
|
||||||
## Common Import Pattern
|
## Common Import Pattern
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { BOLD, RESET, DIM, RED, GREEN, YELLOW } from './cli/core/output.js';
|
import { BOLD, RESET, DIM, RED, GREEN, YELLOW } from './cli/core/output.js';
|
||||||
```
|
```
|
||||||
|
|
||||||
Use dynamic `await import()` for command modules to keep startup fast (lazy loading).
|
Use dynamic `await import()` for command modules to keep startup fast (lazy loading).
|
||||||
|
|
||||||
## History
|
## History
|
||||||
|
|
||||||
- **#237 / PR #244:** 4 commands wired (rc, copilot-bridge, init-remote, rc-tunnel). aspire, link, loop, hire were already present.
|
- **#237 / PR #244:** 4 commands wired (rc, copilot-bridge, init-remote, rc-tunnel). aspire, link, loop, hire were already present.
|
||||||
|
|||||||
@@ -1,89 +1,89 @@
|
|||||||
---
|
---
|
||||||
name: "client-compatibility"
|
name: "client-compatibility"
|
||||||
description: "Platform detection and adaptive spawning for CLI vs VS Code vs other surfaces"
|
description: "Platform detection and adaptive spawning for CLI vs VS Code vs other surfaces"
|
||||||
domain: "orchestration"
|
domain: "orchestration"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "extracted"
|
source: "extracted"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Squad runs on multiple Copilot surfaces (CLI, VS Code, JetBrains, GitHub.com). The coordinator must detect its platform and adapt spawning behavior accordingly. Different tools are available on different platforms, requiring conditional logic for agent spawning, SQL usage, and response timing.
|
Squad runs on multiple Copilot surfaces (CLI, VS Code, JetBrains, GitHub.com). The coordinator must detect its platform and adapt spawning behavior accordingly. Different tools are available on different platforms, requiring conditional logic for agent spawning, SQL usage, and response timing.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### Platform Detection
|
### Platform Detection
|
||||||
|
|
||||||
Before spawning agents, determine the platform by checking available tools:
|
Before spawning agents, determine the platform by checking available tools:
|
||||||
|
|
||||||
1. **CLI mode** — `task` tool is available → full spawning control. Use `task` with `agent_type`, `mode`, `model`, `description`, `prompt` parameters. Collect results via `read_agent`.
|
1. **CLI mode** — `task` tool is available → full spawning control. Use `task` with `agent_type`, `mode`, `model`, `description`, `prompt` parameters. Collect results via `read_agent`.
|
||||||
|
|
||||||
2. **VS Code mode** — `runSubagent` or `agent` tool is available → conditional behavior. Use `runSubagent` with the task prompt. Drop `agent_type`, `mode`, and `model` parameters. Multiple subagents in one turn run concurrently (equivalent to background mode). Results return automatically — no `read_agent` needed.
|
2. **VS Code mode** — `runSubagent` or `agent` tool is available → conditional behavior. Use `runSubagent` with the task prompt. Drop `agent_type`, `mode`, and `model` parameters. Multiple subagents in one turn run concurrently (equivalent to background mode). Results return automatically — no `read_agent` needed.
|
||||||
|
|
||||||
3. **Fallback mode** — neither `task` nor `runSubagent`/`agent` available → work inline. Do not apologize or explain the limitation. Execute the task directly.
|
3. **Fallback mode** — neither `task` nor `runSubagent`/`agent` available → work inline. Do not apologize or explain the limitation. Execute the task directly.
|
||||||
|
|
||||||
If both `task` and `runSubagent` are available, prefer `task` (richer parameter surface).
|
If both `task` and `runSubagent` are available, prefer `task` (richer parameter surface).
|
||||||
|
|
||||||
### VS Code Spawn Adaptations
|
### VS Code Spawn Adaptations
|
||||||
|
|
||||||
When in VS Code mode, the coordinator changes behavior in these ways:
|
When in VS Code mode, the coordinator changes behavior in these ways:
|
||||||
|
|
||||||
- **Spawning tool:** Use `runSubagent` instead of `task`. The prompt is the only required parameter — pass the full agent prompt (charter, identity, task, hygiene, response order) exactly as you would on CLI.
|
- **Spawning tool:** Use `runSubagent` instead of `task`. The prompt is the only required parameter — pass the full agent prompt (charter, identity, task, hygiene, response order) exactly as you would on CLI.
|
||||||
- **Parallelism:** Spawn ALL concurrent agents in a SINGLE turn. They run in parallel automatically. This replaces `mode: "background"` + `read_agent` polling.
|
- **Parallelism:** Spawn ALL concurrent agents in a SINGLE turn. They run in parallel automatically. This replaces `mode: "background"` + `read_agent` polling.
|
||||||
- **Model selection:** Accept the session model. Do NOT attempt per-spawn model selection or fallback chains — they only work on CLI. In Phase 1, all subagents use whatever model the user selected in VS Code's model picker.
|
- **Model selection:** Accept the session model. Do NOT attempt per-spawn model selection or fallback chains — they only work on CLI. In Phase 1, all subagents use whatever model the user selected in VS Code's model picker.
|
||||||
- **Scribe:** Cannot fire-and-forget. Batch Scribe as the LAST subagent in any parallel group. Scribe is light work (file ops only), so the blocking is tolerable.
|
- **Scribe:** Cannot fire-and-forget. Batch Scribe as the LAST subagent in any parallel group. Scribe is light work (file ops only), so the blocking is tolerable.
|
||||||
- **Launch table:** Skip it. Results arrive with the response, not separately. By the time the coordinator speaks, the work is already done.
|
- **Launch table:** Skip it. Results arrive with the response, not separately. By the time the coordinator speaks, the work is already done.
|
||||||
- **`read_agent`:** Skip entirely. Results return automatically when subagents complete.
|
- **`read_agent`:** Skip entirely. Results return automatically when subagents complete.
|
||||||
- **`agent_type`:** Drop it. All VS Code subagents have full tool access by default. Subagents inherit the parent's tools.
|
- **`agent_type`:** Drop it. All VS Code subagents have full tool access by default. Subagents inherit the parent's tools.
|
||||||
- **`description`:** Drop it. The agent name is already in the prompt.
|
- **`description`:** Drop it. The agent name is already in the prompt.
|
||||||
- **Prompt content:** Keep ALL prompt structure — charter, identity, task, hygiene, response order blocks are surface-independent.
|
- **Prompt content:** Keep ALL prompt structure — charter, identity, task, hygiene, response order blocks are surface-independent.
|
||||||
|
|
||||||
### Feature Degradation Table
|
### Feature Degradation Table
|
||||||
|
|
||||||
| Feature | CLI | VS Code | Degradation |
|
| Feature | CLI | VS Code | Degradation |
|
||||||
|---------|-----|---------|-------------|
|
|---------|-----|---------|-------------|
|
||||||
| Parallel fan-out | `mode: "background"` + `read_agent` | Multiple subagents in one turn | None — equivalent concurrency |
|
| Parallel fan-out | `mode: "background"` + `read_agent` | Multiple subagents in one turn | None — equivalent concurrency |
|
||||||
| Model selection | Per-spawn `model` param (4-layer hierarchy) | Session model only (Phase 1) | Accept session model, log intent |
|
| Model selection | Per-spawn `model` param (4-layer hierarchy) | Session model only (Phase 1) | Accept session model, log intent |
|
||||||
| Scribe fire-and-forget | Background, never read | Sync, must wait | Batch with last parallel group |
|
| Scribe fire-and-forget | Background, never read | Sync, must wait | Batch with last parallel group |
|
||||||
| Launch table UX | Show table → results later | Skip table → results with response | UX only — results are correct |
|
| Launch table UX | Show table → results later | Skip table → results with response | UX only — results are correct |
|
||||||
| SQL tool | Available | Not available | Avoid SQL in cross-platform code paths |
|
| SQL tool | Available | Not available | Avoid SQL in cross-platform code paths |
|
||||||
| Response order bug | Critical workaround | Possibly necessary (unverified) | Keep the block — harmless if unnecessary |
|
| Response order bug | Critical workaround | Possibly necessary (unverified) | Keep the block — harmless if unnecessary |
|
||||||
|
|
||||||
### SQL Tool Caveat
|
### SQL Tool Caveat
|
||||||
|
|
||||||
The `sql` tool is **CLI-only**. It does not exist on VS Code, JetBrains, or GitHub.com. Any coordinator logic or agent workflow that depends on SQL (todo tracking, batch processing, session state) will silently fail on non-CLI surfaces. Cross-platform code paths must not depend on SQL. Use filesystem-based state (`.squad/` files) for anything that must work everywhere.
|
The `sql` tool is **CLI-only**. It does not exist on VS Code, JetBrains, or GitHub.com. Any coordinator logic or agent workflow that depends on SQL (todo tracking, batch processing, session state) will silently fail on non-CLI surfaces. Cross-platform code paths must not depend on SQL. Use filesystem-based state (`.squad/` files) for anything that must work everywhere.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
**Example 1: CLI parallel spawn**
|
**Example 1: CLI parallel spawn**
|
||||||
```typescript
|
```typescript
|
||||||
// Coordinator detects task tool available → CLI mode
|
// Coordinator detects task tool available → CLI mode
|
||||||
task({ agent_type: "general-purpose", mode: "background", model: "claude-sonnet-4.5", ... })
|
task({ agent_type: "general-purpose", mode: "background", model: "claude-sonnet-4.5", ... })
|
||||||
task({ agent_type: "general-purpose", mode: "background", model: "claude-haiku-4.5", ... })
|
task({ agent_type: "general-purpose", mode: "background", model: "claude-haiku-4.5", ... })
|
||||||
// Later: read_agent for both
|
// Later: read_agent for both
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example 2: VS Code parallel spawn**
|
**Example 2: VS Code parallel spawn**
|
||||||
```typescript
|
```typescript
|
||||||
// Coordinator detects runSubagent available → VS Code mode
|
// Coordinator detects runSubagent available → VS Code mode
|
||||||
runSubagent({ prompt: "...Fenster charter + task..." })
|
runSubagent({ prompt: "...Fenster charter + task..." })
|
||||||
runSubagent({ prompt: "...Hockney charter + task..." })
|
runSubagent({ prompt: "...Hockney charter + task..." })
|
||||||
runSubagent({ prompt: "...Scribe charter + task..." }) // Last in group
|
runSubagent({ prompt: "...Scribe charter + task..." }) // Last in group
|
||||||
// Results return automatically, no read_agent
|
// Results return automatically, no read_agent
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example 3: Fallback mode**
|
**Example 3: Fallback mode**
|
||||||
```typescript
|
```typescript
|
||||||
// Neither task nor runSubagent available → work inline
|
// Neither task nor runSubagent available → work inline
|
||||||
// Coordinator executes the task directly without spawning
|
// Coordinator executes the task directly without spawning
|
||||||
```
|
```
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- ❌ Using SQL tool in cross-platform workflows (breaks on VS Code/JetBrains/GitHub.com)
|
- ❌ Using SQL tool in cross-platform workflows (breaks on VS Code/JetBrains/GitHub.com)
|
||||||
- ❌ Attempting per-spawn model selection on VS Code (Phase 1 — only session model works)
|
- ❌ Attempting per-spawn model selection on VS Code (Phase 1 — only session model works)
|
||||||
- ❌ Fire-and-forget Scribe on VS Code (must batch as last subagent)
|
- ❌ Fire-and-forget Scribe on VS Code (must batch as last subagent)
|
||||||
- ❌ Showing launch table on VS Code (results already inline)
|
- ❌ Showing launch table on VS Code (results already inline)
|
||||||
- ❌ Apologizing or explaining platform limitations to the user
|
- ❌ Apologizing or explaining platform limitations to the user
|
||||||
- ❌ Using `task` when only `runSubagent` is available
|
- ❌ Using `task` when only `runSubagent` is available
|
||||||
- ❌ Dropping prompt structure (charter/identity/task) on non-CLI platforms
|
- ❌ Dropping prompt structure (charter/identity/task) on non-CLI platforms
|
||||||
|
|||||||
@@ -1,114 +1,114 @@
|
|||||||
---
|
---
|
||||||
name: "cross-squad"
|
name: "cross-squad"
|
||||||
description: "Coordinating work across multiple Squad instances"
|
description: "Coordinating work across multiple Squad instances"
|
||||||
domain: "orchestration"
|
domain: "orchestration"
|
||||||
confidence: "medium"
|
confidence: "medium"
|
||||||
source: "manual"
|
source: "manual"
|
||||||
tools:
|
tools:
|
||||||
- name: "squad-discover"
|
- name: "squad-discover"
|
||||||
description: "List known squads and their capabilities"
|
description: "List known squads and their capabilities"
|
||||||
when: "When you need to find which squad can handle a task"
|
when: "When you need to find which squad can handle a task"
|
||||||
- name: "squad-delegate"
|
- name: "squad-delegate"
|
||||||
description: "Create work in another squad's repository"
|
description: "Create work in another squad's repository"
|
||||||
when: "When a task belongs to another squad's domain"
|
when: "When a task belongs to another squad's domain"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
When an organization runs multiple Squad instances (e.g., platform-squad, frontend-squad, data-squad), those squads need to discover each other, share context, and hand off work across repository boundaries. This skill teaches agents how to coordinate across squads without creating tight coupling.
|
When an organization runs multiple Squad instances (e.g., platform-squad, frontend-squad, data-squad), those squads need to discover each other, share context, and hand off work across repository boundaries. This skill teaches agents how to coordinate across squads without creating tight coupling.
|
||||||
|
|
||||||
Cross-squad orchestration applies when:
|
Cross-squad orchestration applies when:
|
||||||
- A task requires capabilities owned by another squad
|
- A task requires capabilities owned by another squad
|
||||||
- An architectural decision affects multiple squads
|
- An architectural decision affects multiple squads
|
||||||
- A feature spans multiple repositories with different squads
|
- A feature spans multiple repositories with different squads
|
||||||
- A squad needs to request infrastructure, tooling, or support from another squad
|
- A squad needs to request infrastructure, tooling, or support from another squad
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### Discovery via Manifest
|
### Discovery via Manifest
|
||||||
Each squad publishes a `.squad/manifest.json` declaring its name, capabilities, and contact information. Squads discover each other through:
|
Each squad publishes a `.squad/manifest.json` declaring its name, capabilities, and contact information. Squads discover each other through:
|
||||||
1. **Well-known paths**: Check `.squad/manifest.json` in known org repos
|
1. **Well-known paths**: Check `.squad/manifest.json` in known org repos
|
||||||
2. **Upstream config**: Squads already listed in `.squad/upstream.json` are checked for manifests
|
2. **Upstream config**: Squads already listed in `.squad/upstream.json` are checked for manifests
|
||||||
3. **Explicit registry**: A central `squad-registry.json` can list all squads in an org
|
3. **Explicit registry**: A central `squad-registry.json` can list all squads in an org
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "platform-squad",
|
"name": "platform-squad",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Platform infrastructure team",
|
"description": "Platform infrastructure team",
|
||||||
"capabilities": ["kubernetes", "helm", "monitoring", "ci-cd"],
|
"capabilities": ["kubernetes", "helm", "monitoring", "ci-cd"],
|
||||||
"contact": {
|
"contact": {
|
||||||
"repo": "org/platform",
|
"repo": "org/platform",
|
||||||
"labels": ["squad:platform"]
|
"labels": ["squad:platform"]
|
||||||
},
|
},
|
||||||
"accepts": ["issues", "prs"],
|
"accepts": ["issues", "prs"],
|
||||||
"skills": ["helm-developer", "operator-developer", "pipeline-engineer"]
|
"skills": ["helm-developer", "operator-developer", "pipeline-engineer"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Context Sharing
|
### Context Sharing
|
||||||
When delegating work, share only what the target squad needs:
|
When delegating work, share only what the target squad needs:
|
||||||
- **Capability list**: What this squad can do (from manifest)
|
- **Capability list**: What this squad can do (from manifest)
|
||||||
- **Relevant decisions**: Only decisions that affect the target squad
|
- **Relevant decisions**: Only decisions that affect the target squad
|
||||||
- **Handoff context**: A concise description of why this work is being delegated
|
- **Handoff context**: A concise description of why this work is being delegated
|
||||||
|
|
||||||
Do NOT share:
|
Do NOT share:
|
||||||
- Internal team state (casting history, session logs)
|
- Internal team state (casting history, session logs)
|
||||||
- Full decision archives (send only relevant excerpts)
|
- Full decision archives (send only relevant excerpts)
|
||||||
- Authentication credentials or secrets
|
- Authentication credentials or secrets
|
||||||
|
|
||||||
### Work Handoff Protocol
|
### Work Handoff Protocol
|
||||||
1. **Check manifest**: Verify the target squad accepts the work type (issues, PRs)
|
1. **Check manifest**: Verify the target squad accepts the work type (issues, PRs)
|
||||||
2. **Create issue**: Use `gh issue create` in the target repo with:
|
2. **Create issue**: Use `gh issue create` in the target repo with:
|
||||||
- Title: `[cross-squad] <description>`
|
- Title: `[cross-squad] <description>`
|
||||||
- Label: `squad:cross-squad` (or the squad's configured label)
|
- Label: `squad:cross-squad` (or the squad's configured label)
|
||||||
- Body: Context, acceptance criteria, and link back to originating issue
|
- Body: Context, acceptance criteria, and link back to originating issue
|
||||||
3. **Track**: Record the cross-squad issue URL in the originating squad's orchestration log
|
3. **Track**: Record the cross-squad issue URL in the originating squad's orchestration log
|
||||||
4. **Poll**: Periodically check if the delegated issue is closed/completed
|
4. **Poll**: Periodically check if the delegated issue is closed/completed
|
||||||
|
|
||||||
### Feedback Loop
|
### Feedback Loop
|
||||||
Track delegated work completion:
|
Track delegated work completion:
|
||||||
- Poll target issue status via `gh issue view`
|
- Poll target issue status via `gh issue view`
|
||||||
- Update originating issue with status changes
|
- Update originating issue with status changes
|
||||||
- Close the feedback loop when delegated work merges
|
- Close the feedback loop when delegated work merges
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Discovering squads
|
### Discovering squads
|
||||||
```bash
|
```bash
|
||||||
# List all squads discoverable from upstreams and known repos
|
# List all squads discoverable from upstreams and known repos
|
||||||
squad discover
|
squad discover
|
||||||
|
|
||||||
# Output:
|
# Output:
|
||||||
# platform-squad → org/platform (kubernetes, helm, monitoring)
|
# platform-squad → org/platform (kubernetes, helm, monitoring)
|
||||||
# frontend-squad → org/frontend (react, nextjs, storybook)
|
# frontend-squad → org/frontend (react, nextjs, storybook)
|
||||||
# data-squad → org/data (spark, airflow, dbt)
|
# data-squad → org/data (spark, airflow, dbt)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Delegating work
|
### Delegating work
|
||||||
```bash
|
```bash
|
||||||
# Delegate a task to the platform squad
|
# Delegate a task to the platform squad
|
||||||
squad delegate platform-squad "Add Prometheus metrics endpoint for the auth service"
|
squad delegate platform-squad "Add Prometheus metrics endpoint for the auth service"
|
||||||
|
|
||||||
# Creates issue in org/platform with cross-squad label and context
|
# Creates issue in org/platform with cross-squad label and context
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manifest in squad.config.ts
|
### Manifest in squad.config.ts
|
||||||
```typescript
|
```typescript
|
||||||
export default defineSquad({
|
export default defineSquad({
|
||||||
manifest: {
|
manifest: {
|
||||||
name: 'platform-squad',
|
name: 'platform-squad',
|
||||||
capabilities: ['kubernetes', 'helm'],
|
capabilities: ['kubernetes', 'helm'],
|
||||||
contact: { repo: 'org/platform', labels: ['squad:platform'] },
|
contact: { repo: 'org/platform', labels: ['squad:platform'] },
|
||||||
accepts: ['issues', 'prs'],
|
accepts: ['issues', 'prs'],
|
||||||
skills: ['helm-developer', 'operator-developer'],
|
skills: ['helm-developer', 'operator-developer'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
- **Direct file writes across repos** — Never modify another squad's `.squad/` directory. Use issues and PRs as the communication protocol.
|
- **Direct file writes across repos** — Never modify another squad's `.squad/` directory. Use issues and PRs as the communication protocol.
|
||||||
- **Tight coupling** — Don't depend on another squad's internal structure. Use the manifest as the public API contract.
|
- **Tight coupling** — Don't depend on another squad's internal structure. Use the manifest as the public API contract.
|
||||||
- **Unbounded delegation** — Always include acceptance criteria and a timeout. Don't create open-ended requests.
|
- **Unbounded delegation** — Always include acceptance criteria and a timeout. Don't create open-ended requests.
|
||||||
- **Skipping discovery** — Don't hardcode squad locations. Use manifests and the discovery protocol.
|
- **Skipping discovery** — Don't hardcode squad locations. Use manifests and the discovery protocol.
|
||||||
- **Sharing secrets** — Never include credentials, tokens, or internal URLs in cross-squad issues.
|
- **Sharing secrets** — Never include credentials, tokens, or internal URLs in cross-squad issues.
|
||||||
- **Circular delegation** — Track delegation chains. If squad A delegates to B which delegates back to A, something is wrong.
|
- **Circular delegation** — Track delegation chains. If squad A delegates to B which delegates back to A, something is wrong.
|
||||||
|
|||||||
@@ -1,287 +1,287 @@
|
|||||||
---
|
---
|
||||||
name: "distributed-mesh"
|
name: "distributed-mesh"
|
||||||
description: "How to coordinate with squads on different machines using git as transport"
|
description: "How to coordinate with squads on different machines using git as transport"
|
||||||
domain: "distributed-coordination"
|
domain: "distributed-coordination"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "multi-model-consensus (Opus 4.6, Sonnet 4.5, GPT-5.4)"
|
source: "multi-model-consensus (Opus 4.6, Sonnet 4.5, GPT-5.4)"
|
||||||
---
|
---
|
||||||
|
|
||||||
## SCOPE
|
## SCOPE
|
||||||
|
|
||||||
**✅ THIS SKILL PRODUCES (exactly these, nothing more):**
|
**✅ THIS SKILL PRODUCES (exactly these, nothing more):**
|
||||||
|
|
||||||
1. **`mesh.json`** — Generated from user answers about zones and squads (which squads participate, what zone each is in, paths/URLs for each), using `mesh.json.example` in this skill's directory as the schema template
|
1. **`mesh.json`** — Generated from user answers about zones and squads (which squads participate, what zone each is in, paths/URLs for each), using `mesh.json.example` in this skill's directory as the schema template
|
||||||
2. **`sync-mesh.sh` and `sync-mesh.ps1`** — Copied from this skill's directory into the project root (these are bundled resources, NOT generated code)
|
2. **`sync-mesh.sh` and `sync-mesh.ps1`** — Copied from this skill's directory into the project root (these are bundled resources, NOT generated code)
|
||||||
3. **Zone 2 state repo initialization** (if applicable) — If the user specified a Zone 2 shared state repo, run `sync-mesh.sh --init` to scaffold the state repo structure
|
3. **Zone 2 state repo initialization** (if applicable) — If the user specified a Zone 2 shared state repo, run `sync-mesh.sh --init` to scaffold the state repo structure
|
||||||
4. **A decision entry** in `.squad/decisions/inbox/` documenting the mesh configuration for team awareness
|
4. **A decision entry** in `.squad/decisions/inbox/` documenting the mesh configuration for team awareness
|
||||||
|
|
||||||
**❌ THIS SKILL DOES NOT PRODUCE:**
|
**❌ THIS SKILL DOES NOT PRODUCE:**
|
||||||
|
|
||||||
- **No application code** — No validators, libraries, or modules of any kind
|
- **No application code** — No validators, libraries, or modules of any kind
|
||||||
- **No test files** — No test suites, test cases, or test scaffolding
|
- **No test files** — No test suites, test cases, or test scaffolding
|
||||||
- **No GENERATING sync scripts** — They are bundled with this skill as pre-built resources. COPY them, don't generate them.
|
- **No GENERATING sync scripts** — They are bundled with this skill as pre-built resources. COPY them, don't generate them.
|
||||||
- **No daemons or services** — No background processes, servers, or persistent runtimes
|
- **No daemons or services** — No background processes, servers, or persistent runtimes
|
||||||
- **No modifications to existing squad files** beyond the decision entry (no changes to team.md, routing.md, agent charters, etc.)
|
- **No modifications to existing squad files** beyond the decision entry (no changes to team.md, routing.md, agent charters, etc.)
|
||||||
|
|
||||||
**Your role:** Configure the mesh topology and install the bundled sync scripts. Nothing more.
|
**Your role:** Configure the mesh topology and install the bundled sync scripts. Nothing more.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
When squads are on different machines (developer laptops, CI runners, cloud VMs, partner orgs), the local file-reading convention still works — but remote files need to arrive on your disk first. This skill teaches the pattern for distributed squad communication.
|
When squads are on different machines (developer laptops, CI runners, cloud VMs, partner orgs), the local file-reading convention still works — but remote files need to arrive on your disk first. This skill teaches the pattern for distributed squad communication.
|
||||||
|
|
||||||
**When this applies:**
|
**When this applies:**
|
||||||
- Squads span multiple machines, VMs, or CI runners
|
- Squads span multiple machines, VMs, or CI runners
|
||||||
- Squads span organizations or companies
|
- Squads span organizations or companies
|
||||||
- An agent needs context from a squad whose files aren't on the local filesystem
|
- An agent needs context from a squad whose files aren't on the local filesystem
|
||||||
|
|
||||||
**When this does NOT apply:**
|
**When this does NOT apply:**
|
||||||
- All squads are on the same machine (just read the files directly)
|
- All squads are on the same machine (just read the files directly)
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### The Core Principle
|
### The Core Principle
|
||||||
|
|
||||||
> "The filesystem is the mesh, and git is how the mesh crosses machine boundaries."
|
> "The filesystem is the mesh, and git is how the mesh crosses machine boundaries."
|
||||||
|
|
||||||
The agent interface never changes. Agents always read local files. The distributed layer's only job is to make remote files appear locally before the agent reads them.
|
The agent interface never changes. Agents always read local files. The distributed layer's only job is to make remote files appear locally before the agent reads them.
|
||||||
|
|
||||||
### Three Zones of Communication
|
### Three Zones of Communication
|
||||||
|
|
||||||
**Zone 1 — Local:** Same filesystem. Read files directly. Zero transport.
|
**Zone 1 — Local:** Same filesystem. Read files directly. Zero transport.
|
||||||
|
|
||||||
**Zone 2 — Remote-Trusted:** Different host, same org, shared git auth. Transport: `git pull` from a shared repo. This collapses Zone 2 into Zone 1 — files materialize on disk, agent reads them normally.
|
**Zone 2 — Remote-Trusted:** Different host, same org, shared git auth. Transport: `git pull` from a shared repo. This collapses Zone 2 into Zone 1 — files materialize on disk, agent reads them normally.
|
||||||
|
|
||||||
**Zone 3 — Remote-Opaque:** Different org, no shared auth. Transport: `curl` to fetch published contracts (SUMMARY.md). One-way visibility — you see only what they publish.
|
**Zone 3 — Remote-Opaque:** Different org, no shared auth. Transport: `curl` to fetch published contracts (SUMMARY.md). One-way visibility — you see only what they publish.
|
||||||
|
|
||||||
### Agent Lifecycle (Distributed)
|
### Agent Lifecycle (Distributed)
|
||||||
|
|
||||||
```
|
```
|
||||||
1. SYNC: git pull (Zone 2) + curl (Zone 3) — materialize remote state
|
1. SYNC: git pull (Zone 2) + curl (Zone 3) — materialize remote state
|
||||||
2. READ: cat .mesh/**/state.md — all files are local now
|
2. READ: cat .mesh/**/state.md — all files are local now
|
||||||
3. WORK: do their assigned work (the agent's normal task, NOT mesh-building)
|
3. WORK: do their assigned work (the agent's normal task, NOT mesh-building)
|
||||||
4. WRITE: update own billboard, log, drops
|
4. WRITE: update own billboard, log, drops
|
||||||
5. PUBLISH: git add + commit + push — share state with remote peers
|
5. PUBLISH: git add + commit + push — share state with remote peers
|
||||||
```
|
```
|
||||||
|
|
||||||
Steps 2–4 are identical to local-only. Steps 1 and 5 are the entire distributed extension. **Note:** "WORK" means the agent performs its normal squad duties — it does NOT mean "build mesh infrastructure."
|
Steps 2–4 are identical to local-only. Steps 1 and 5 are the entire distributed extension. **Note:** "WORK" means the agent performs its normal squad duties — it does NOT mean "build mesh infrastructure."
|
||||||
|
|
||||||
### The mesh.json Config
|
### The mesh.json Config
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"squads": {
|
"squads": {
|
||||||
"auth-squad": { "zone": "local", "path": "../auth-squad/.mesh" },
|
"auth-squad": { "zone": "local", "path": "../auth-squad/.mesh" },
|
||||||
"ci-squad": {
|
"ci-squad": {
|
||||||
"zone": "remote-trusted",
|
"zone": "remote-trusted",
|
||||||
"source": "git@github.com:our-org/ci-squad.git",
|
"source": "git@github.com:our-org/ci-squad.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"sync_to": ".mesh/remotes/ci-squad"
|
"sync_to": ".mesh/remotes/ci-squad"
|
||||||
},
|
},
|
||||||
"partner-fraud": {
|
"partner-fraud": {
|
||||||
"zone": "remote-opaque",
|
"zone": "remote-opaque",
|
||||||
"source": "https://partner.dev/squad-contracts/fraud/SUMMARY.md",
|
"source": "https://partner.dev/squad-contracts/fraud/SUMMARY.md",
|
||||||
"sync_to": ".mesh/remotes/partner-fraud",
|
"sync_to": ".mesh/remotes/partner-fraud",
|
||||||
"auth": "bearer"
|
"auth": "bearer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Three zone types, one file. Local squads need only a path. Remote-trusted need a git URL. Remote-opaque need an HTTP URL.
|
Three zone types, one file. Local squads need only a path. Remote-trusted need a git URL. Remote-opaque need an HTTP URL.
|
||||||
|
|
||||||
### Write Partitioning
|
### Write Partitioning
|
||||||
|
|
||||||
Each squad writes only to its own directory (`boards/{self}.md`, `squads/{self}/*`, `drops/{date}-{self}-*.md`). No two squads write to the same file. Git push/pull never conflicts. If push fails ("branch is behind"), the fix is always `git pull --rebase && git push`.
|
Each squad writes only to its own directory (`boards/{self}.md`, `squads/{self}/*`, `drops/{date}-{self}-*.md`). No two squads write to the same file. Git push/pull never conflicts. If push fails ("branch is behind"), the fix is always `git pull --rebase && git push`.
|
||||||
|
|
||||||
### Trust Boundaries
|
### Trust Boundaries
|
||||||
|
|
||||||
Trust maps to git permissions:
|
Trust maps to git permissions:
|
||||||
- **Same repo access** = full mesh visibility
|
- **Same repo access** = full mesh visibility
|
||||||
- **Read-only access** = can observe, can't write
|
- **Read-only access** = can observe, can't write
|
||||||
- **No access** = invisible (correct behavior)
|
- **No access** = invisible (correct behavior)
|
||||||
|
|
||||||
For selective visibility, use separate repos per audience (internal, partner, public). Git permissions ARE the trust negotiation.
|
For selective visibility, use separate repos per audience (internal, partner, public). Git permissions ARE the trust negotiation.
|
||||||
|
|
||||||
### Phased Rollout
|
### Phased Rollout
|
||||||
|
|
||||||
- **Phase 0:** Convention only — document zones, agree on mesh.json fields, manually run `git pull`/`git push`. Zero new code.
|
- **Phase 0:** Convention only — document zones, agree on mesh.json fields, manually run `git pull`/`git push`. Zero new code.
|
||||||
- **Phase 1:** Sync script (~30 lines bash or PowerShell) when manual sync gets tedious.
|
- **Phase 1:** Sync script (~30 lines bash or PowerShell) when manual sync gets tedious.
|
||||||
- **Phase 2:** Published contracts + curl fetch when a Zone 3 partner appears.
|
- **Phase 2:** Published contracts + curl fetch when a Zone 3 partner appears.
|
||||||
- **Phase 3:** Never. No MCP federation, A2A, service discovery, message queues.
|
- **Phase 3:** Never. No MCP federation, A2A, service discovery, message queues.
|
||||||
|
|
||||||
**Important:** Phases are NOT auto-advanced. These are project-level decisions — you start at Phase 0 (manual sync) and only move forward when the team decides complexity is justified.
|
**Important:** Phases are NOT auto-advanced. These are project-level decisions — you start at Phase 0 (manual sync) and only move forward when the team decides complexity is justified.
|
||||||
|
|
||||||
### Mesh State Repo
|
### Mesh State Repo
|
||||||
|
|
||||||
The shared mesh state repo is a plain git repository — NOT a Squad project. It holds:
|
The shared mesh state repo is a plain git repository — NOT a Squad project. It holds:
|
||||||
- One directory per participating squad
|
- One directory per participating squad
|
||||||
- Each directory contains at minimum a SUMMARY.md with the squad's current state
|
- Each directory contains at minimum a SUMMARY.md with the squad's current state
|
||||||
- A root README explaining what the repo is and who participates
|
- A root README explaining what the repo is and who participates
|
||||||
|
|
||||||
No `.squad/` folder, no agents, no automation. Write partitioning means each squad only pushes to its own directory. The repo is a rendezvous point, not an intelligent system.
|
No `.squad/` folder, no agents, no automation. Write partitioning means each squad only pushes to its own directory. The repo is a rendezvous point, not an intelligent system.
|
||||||
|
|
||||||
If you want a squad that *observes* mesh health, that's a separate Squad project that lists the state repo as a Zone 2 remote in its `mesh.json` — it does NOT live inside the state repo.
|
If you want a squad that *observes* mesh health, that's a separate Squad project that lists the state repo as a Zone 2 remote in its `mesh.json` — it does NOT live inside the state repo.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Developer Laptop + CI Squad (Zone 2)
|
### Developer Laptop + CI Squad (Zone 2)
|
||||||
|
|
||||||
Auth-squad agent wakes up. `git pull` brings ci-squad's latest results. Agent reads: "3 test failures in auth module." Adjusts work. Pushes results when done. **Overhead: one `git pull`, one `git push`.**
|
Auth-squad agent wakes up. `git pull` brings ci-squad's latest results. Agent reads: "3 test failures in auth module." Adjusts work. Pushes results when done. **Overhead: one `git pull`, one `git push`.**
|
||||||
|
|
||||||
### Two Orgs Collaborating (Zone 3)
|
### Two Orgs Collaborating (Zone 3)
|
||||||
|
|
||||||
Payment-squad fetches partner's published SUMMARY.md via curl. Reads: "Risk scoring v3 API deprecated April 15. New field `device_fingerprint` required." The consuming agent (in payment-squad's team) reads this information and uses it to inform its work — for example, updating payment integration code to include the new field. Partner can't see payment-squad's internals.
|
Payment-squad fetches partner's published SUMMARY.md via curl. Reads: "Risk scoring v3 API deprecated April 15. New field `device_fingerprint` required." The consuming agent (in payment-squad's team) reads this information and uses it to inform its work — for example, updating payment integration code to include the new field. Partner can't see payment-squad's internals.
|
||||||
|
|
||||||
### Same Org, Shared Mesh Repo (Zone 2)
|
### Same Org, Shared Mesh Repo (Zone 2)
|
||||||
|
|
||||||
Three squads on different machines. One shared git repo holds the mesh. Each squad: `git pull` before work, `git push` after. Write partitioning ensures zero merge conflicts.
|
Three squads on different machines. One shared git repo holds the mesh. Each squad: `git pull` before work, `git push` after. Write partitioning ensures zero merge conflicts.
|
||||||
|
|
||||||
## AGENT WORKFLOW (Deterministic Setup)
|
## AGENT WORKFLOW (Deterministic Setup)
|
||||||
|
|
||||||
When a user invokes this skill to set up a distributed mesh, follow these steps **exactly, in order:**
|
When a user invokes this skill to set up a distributed mesh, follow these steps **exactly, in order:**
|
||||||
|
|
||||||
### Step 1: ASK the user for mesh topology
|
### Step 1: ASK the user for mesh topology
|
||||||
|
|
||||||
Ask these questions (adapt phrasing naturally, but get these answers):
|
Ask these questions (adapt phrasing naturally, but get these answers):
|
||||||
|
|
||||||
1. **Which squads are participating?** (List of squad names)
|
1. **Which squads are participating?** (List of squad names)
|
||||||
2. **For each squad, which zone is it in?**
|
2. **For each squad, which zone is it in?**
|
||||||
- `local` — same filesystem (just need a path)
|
- `local` — same filesystem (just need a path)
|
||||||
- `remote-trusted` — different machine, same org, shared git access (need git URL + ref)
|
- `remote-trusted` — different machine, same org, shared git access (need git URL + ref)
|
||||||
- `remote-opaque` — different org, no shared auth (need HTTPS URL to published contract)
|
- `remote-opaque` — different org, no shared auth (need HTTPS URL to published contract)
|
||||||
3. **For each squad, what's the connection info?**
|
3. **For each squad, what's the connection info?**
|
||||||
- Local: relative or absolute path to their `.mesh/` directory
|
- Local: relative or absolute path to their `.mesh/` directory
|
||||||
- Remote-trusted: git URL (SSH or HTTPS), ref (branch/tag), and where to sync it to locally
|
- Remote-trusted: git URL (SSH or HTTPS), ref (branch/tag), and where to sync it to locally
|
||||||
- Remote-opaque: HTTPS URL to their SUMMARY.md, where to sync it, and auth type (none/bearer)
|
- Remote-opaque: HTTPS URL to their SUMMARY.md, where to sync it, and auth type (none/bearer)
|
||||||
4. **Where should the shared state live?** (For Zone 2 squads: git repo URL for the mesh state, or confirm each squad syncs independently)
|
4. **Where should the shared state live?** (For Zone 2 squads: git repo URL for the mesh state, or confirm each squad syncs independently)
|
||||||
|
|
||||||
### Step 2: GENERATE `mesh.json`
|
### Step 2: GENERATE `mesh.json`
|
||||||
|
|
||||||
Using the answers from Step 1, create a `mesh.json` file at the project root. Use `mesh.json.example` from THIS skill's directory (`.squad/skills/distributed-mesh/mesh.json.example`) as the schema template.
|
Using the answers from Step 1, create a `mesh.json` file at the project root. Use `mesh.json.example` from THIS skill's directory (`.squad/skills/distributed-mesh/mesh.json.example`) as the schema template.
|
||||||
|
|
||||||
Structure:
|
Structure:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"squads": {
|
"squads": {
|
||||||
"<squad-name>": { "zone": "local", "path": "<relative-or-absolute-path>" },
|
"<squad-name>": { "zone": "local", "path": "<relative-or-absolute-path>" },
|
||||||
"<squad-name>": {
|
"<squad-name>": {
|
||||||
"zone": "remote-trusted",
|
"zone": "remote-trusted",
|
||||||
"source": "<git-url>",
|
"source": "<git-url>",
|
||||||
"ref": "<branch-or-tag>",
|
"ref": "<branch-or-tag>",
|
||||||
"sync_to": ".mesh/remotes/<squad-name>"
|
"sync_to": ".mesh/remotes/<squad-name>"
|
||||||
},
|
},
|
||||||
"<squad-name>": {
|
"<squad-name>": {
|
||||||
"zone": "remote-opaque",
|
"zone": "remote-opaque",
|
||||||
"source": "<https-url-to-summary>",
|
"source": "<https-url-to-summary>",
|
||||||
"sync_to": ".mesh/remotes/<squad-name>",
|
"sync_to": ".mesh/remotes/<squad-name>",
|
||||||
"auth": "<none|bearer>"
|
"auth": "<none|bearer>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Write this file to the project root. Do NOT write any other code.
|
Write this file to the project root. Do NOT write any other code.
|
||||||
|
|
||||||
### Step 3: COPY sync scripts
|
### Step 3: COPY sync scripts
|
||||||
|
|
||||||
Copy the bundled sync scripts from THIS skill's directory into the project root:
|
Copy the bundled sync scripts from THIS skill's directory into the project root:
|
||||||
|
|
||||||
- **Source:** `.squad/skills/distributed-mesh/sync-mesh.sh`
|
- **Source:** `.squad/skills/distributed-mesh/sync-mesh.sh`
|
||||||
- **Destination:** `sync-mesh.sh` (project root)
|
- **Destination:** `sync-mesh.sh` (project root)
|
||||||
|
|
||||||
- **Source:** `.squad/skills/distributed-mesh/sync-mesh.ps1`
|
- **Source:** `.squad/skills/distributed-mesh/sync-mesh.ps1`
|
||||||
- **Destination:** `sync-mesh.ps1` (project root)
|
- **Destination:** `sync-mesh.ps1` (project root)
|
||||||
|
|
||||||
These are bundled resources. Do NOT generate them — COPY them directly.
|
These are bundled resources. Do NOT generate them — COPY them directly.
|
||||||
|
|
||||||
### Step 4: RUN `--init` (if Zone 2 state repo exists)
|
### Step 4: RUN `--init` (if Zone 2 state repo exists)
|
||||||
|
|
||||||
If the user specified a Zone 2 shared state repo in Step 1, run the initialization:
|
If the user specified a Zone 2 shared state repo in Step 1, run the initialization:
|
||||||
|
|
||||||
**On Unix/Linux/macOS:**
|
**On Unix/Linux/macOS:**
|
||||||
```bash
|
```bash
|
||||||
bash sync-mesh.sh --init
|
bash sync-mesh.sh --init
|
||||||
```
|
```
|
||||||
|
|
||||||
**On Windows:**
|
**On Windows:**
|
||||||
```powershell
|
```powershell
|
||||||
.\sync-mesh.ps1 -Init
|
.\sync-mesh.ps1 -Init
|
||||||
```
|
```
|
||||||
|
|
||||||
This scaffolds the state repo structure (squad directories, placeholder SUMMARY.md files, root README).
|
This scaffolds the state repo structure (squad directories, placeholder SUMMARY.md files, root README).
|
||||||
|
|
||||||
**Skip this step if:**
|
**Skip this step if:**
|
||||||
- No Zone 2 squads are configured (local/opaque only)
|
- No Zone 2 squads are configured (local/opaque only)
|
||||||
- The state repo already exists and is initialized
|
- The state repo already exists and is initialized
|
||||||
|
|
||||||
### Step 5: WRITE a decision entry
|
### Step 5: WRITE a decision entry
|
||||||
|
|
||||||
Create a decision file at `.squad/decisions/inbox/<your-agent-name>-mesh-setup.md` with this content:
|
Create a decision file at `.squad/decisions/inbox/<your-agent-name>-mesh-setup.md` with this content:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
### <YYYY-MM-DD>: Mesh configuration
|
### <YYYY-MM-DD>: Mesh configuration
|
||||||
|
|
||||||
**By:** <your-agent-name> (via distributed-mesh skill)
|
**By:** <your-agent-name> (via distributed-mesh skill)
|
||||||
|
|
||||||
**What:** Configured distributed mesh with <N> squads across zones <list-zones-used>
|
**What:** Configured distributed mesh with <N> squads across zones <list-zones-used>
|
||||||
|
|
||||||
**Squads:**
|
**Squads:**
|
||||||
- `<squad-name>` — Zone <X> — <brief-connection-info>
|
- `<squad-name>` — Zone <X> — <brief-connection-info>
|
||||||
- `<squad-name>` — Zone <X> — <brief-connection-info>
|
- `<squad-name>` — Zone <X> — <brief-connection-info>
|
||||||
- ...
|
- ...
|
||||||
|
|
||||||
**State repo:** <git-url-if-zone-2-used, or "N/A (local/opaque only)">
|
**State repo:** <git-url-if-zone-2-used, or "N/A (local/opaque only)">
|
||||||
|
|
||||||
**Why:** <user's stated reason for setting up the mesh, or "Enable cross-machine squad coordination">
|
**Why:** <user's stated reason for setting up the mesh, or "Enable cross-machine squad coordination">
|
||||||
```
|
```
|
||||||
|
|
||||||
Write this file. The Scribe will merge it into the main decisions file later.
|
Write this file. The Scribe will merge it into the main decisions file later.
|
||||||
|
|
||||||
### Step 6: STOP
|
### Step 6: STOP
|
||||||
|
|
||||||
**You are done.** Do not:
|
**You are done.** Do not:
|
||||||
- Generate sync scripts (they're bundled with this skill — COPY them)
|
- Generate sync scripts (they're bundled with this skill — COPY them)
|
||||||
- Write validator code
|
- Write validator code
|
||||||
- Write test files
|
- Write test files
|
||||||
- Create any other modules, libraries, or application code
|
- Create any other modules, libraries, or application code
|
||||||
- Modify existing squad files (team.md, routing.md, charters)
|
- Modify existing squad files (team.md, routing.md, charters)
|
||||||
- Auto-advance to Phase 2 or Phase 3
|
- Auto-advance to Phase 2 or Phase 3
|
||||||
|
|
||||||
Output a simple completion message:
|
Output a simple completion message:
|
||||||
|
|
||||||
```
|
```
|
||||||
✅ Mesh configured. Created:
|
✅ Mesh configured. Created:
|
||||||
- mesh.json (<N> squads)
|
- mesh.json (<N> squads)
|
||||||
- sync-mesh.sh and sync-mesh.ps1 (copied from skill bundle)
|
- sync-mesh.sh and sync-mesh.ps1 (copied from skill bundle)
|
||||||
- Decision entry: .squad/decisions/inbox/<filename>
|
- Decision entry: .squad/decisions/inbox/<filename>
|
||||||
|
|
||||||
Run `bash sync-mesh.sh` (or `.\sync-mesh.ps1` on Windows) before agents start to materialize remote state.
|
Run `bash sync-mesh.sh` (or `.\sync-mesh.ps1` on Windows) before agents start to materialize remote state.
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
**❌ Code generation anti-patterns:**
|
**❌ Code generation anti-patterns:**
|
||||||
- Writing `mesh-config-validator.js` or any validator module
|
- Writing `mesh-config-validator.js` or any validator module
|
||||||
- Writing test files for mesh configuration
|
- Writing test files for mesh configuration
|
||||||
- Generating sync scripts instead of copying the bundled ones from this skill's directory
|
- Generating sync scripts instead of copying the bundled ones from this skill's directory
|
||||||
- Creating library modules or utilities
|
- Creating library modules or utilities
|
||||||
- Building any code that "runs the mesh" — the mesh is read by agents, not executed
|
- Building any code that "runs the mesh" — the mesh is read by agents, not executed
|
||||||
|
|
||||||
**❌ Architectural anti-patterns:**
|
**❌ Architectural anti-patterns:**
|
||||||
- Building a federation protocol — Git push/pull IS federation
|
- Building a federation protocol — Git push/pull IS federation
|
||||||
- Running a sync daemon or server — Agents are not persistent. Sync at startup, publish at shutdown
|
- Running a sync daemon or server — Agents are not persistent. Sync at startup, publish at shutdown
|
||||||
- Real-time notifications — Agents don't need real-time. They need "recent enough." `git pull` is recent enough
|
- Real-time notifications — Agents don't need real-time. They need "recent enough." `git pull` is recent enough
|
||||||
- Schema validation for markdown — The LLM reads markdown. If the format changes, it adapts
|
- Schema validation for markdown — The LLM reads markdown. If the format changes, it adapts
|
||||||
- Service discovery protocol — mesh.json is a file with 10 entries. Not a "discovery problem"
|
- Service discovery protocol — mesh.json is a file with 10 entries. Not a "discovery problem"
|
||||||
- Auth framework — Git SSH keys and HTTPS tokens. Not a framework. Already configured
|
- Auth framework — Git SSH keys and HTTPS tokens. Not a framework. Already configured
|
||||||
- Message queues / event buses — Agents wake, read, work, write, sleep. Nobody's home to receive events
|
- Message queues / event buses — Agents wake, read, work, write, sleep. Nobody's home to receive events
|
||||||
- Any component requiring a running process — That's the line. Don't cross it
|
- Any component requiring a running process — That's the line. Don't cross it
|
||||||
|
|
||||||
**❌ Scope creep anti-patterns:**
|
**❌ Scope creep anti-patterns:**
|
||||||
- Auto-advancing phases without user decision
|
- Auto-advancing phases without user decision
|
||||||
- Modifying agent charters or routing rules
|
- Modifying agent charters or routing rules
|
||||||
- Setting up CI/CD pipelines for mesh sync
|
- Setting up CI/CD pipelines for mesh sync
|
||||||
- Creating dashboards or monitoring tools
|
- Creating dashboards or monitoring tools
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
{
|
{
|
||||||
"squads": {
|
"squads": {
|
||||||
"auth-squad": {
|
"auth-squad": {
|
||||||
"zone": "local",
|
"zone": "local",
|
||||||
"path": "../auth-squad/.mesh"
|
"path": "../auth-squad/.mesh"
|
||||||
},
|
},
|
||||||
"api-squad": {
|
"api-squad": {
|
||||||
"zone": "local",
|
"zone": "local",
|
||||||
"path": "../api-squad/.mesh"
|
"path": "../api-squad/.mesh"
|
||||||
},
|
},
|
||||||
"ci-squad": {
|
"ci-squad": {
|
||||||
"zone": "remote-trusted",
|
"zone": "remote-trusted",
|
||||||
"source": "git@github.com:our-org/ci-squad.git",
|
"source": "git@github.com:our-org/ci-squad.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"sync_to": ".mesh/remotes/ci-squad"
|
"sync_to": ".mesh/remotes/ci-squad"
|
||||||
},
|
},
|
||||||
"data-squad": {
|
"data-squad": {
|
||||||
"zone": "remote-trusted",
|
"zone": "remote-trusted",
|
||||||
"source": "git@github.com:our-org/data-pipeline.git",
|
"source": "git@github.com:our-org/data-pipeline.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"sync_to": ".mesh/remotes/data-squad"
|
"sync_to": ".mesh/remotes/data-squad"
|
||||||
},
|
},
|
||||||
"partner-fraud": {
|
"partner-fraud": {
|
||||||
"zone": "remote-opaque",
|
"zone": "remote-opaque",
|
||||||
"source": "https://partner.example.com/squad-contracts/fraud/SUMMARY.md",
|
"source": "https://partner.example.com/squad-contracts/fraud/SUMMARY.md",
|
||||||
"sync_to": ".mesh/remotes/partner-fraud",
|
"sync_to": ".mesh/remotes/partner-fraud",
|
||||||
"auth": "bearer"
|
"auth": "bearer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,111 +1,111 @@
|
|||||||
# sync-mesh.ps1 — Materialize remote squad state locally
|
# sync-mesh.ps1 — Materialize remote squad state locally
|
||||||
#
|
#
|
||||||
# Reads mesh.json, fetches remote squads into local directories.
|
# Reads mesh.json, fetches remote squads into local directories.
|
||||||
# Run before agent reads. No daemon. No service. ~40 lines.
|
# Run before agent reads. No daemon. No service. ~40 lines.
|
||||||
#
|
#
|
||||||
# Usage: .\sync-mesh.ps1 [path-to-mesh.json]
|
# Usage: .\sync-mesh.ps1 [path-to-mesh.json]
|
||||||
# .\sync-mesh.ps1 -Init [path-to-mesh.json]
|
# .\sync-mesh.ps1 -Init [path-to-mesh.json]
|
||||||
# Requires: git
|
# Requires: git
|
||||||
param(
|
param(
|
||||||
[switch]$Init,
|
[switch]$Init,
|
||||||
[string]$MeshJson = "mesh.json"
|
[string]$MeshJson = "mesh.json"
|
||||||
)
|
)
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
# Handle -Init mode
|
# Handle -Init mode
|
||||||
if ($Init) {
|
if ($Init) {
|
||||||
if (-not (Test-Path $MeshJson)) {
|
if (-not (Test-Path $MeshJson)) {
|
||||||
Write-Host "❌ $MeshJson not found"
|
Write-Host "❌ $MeshJson not found"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "🚀 Initializing mesh state repository..."
|
Write-Host "🚀 Initializing mesh state repository..."
|
||||||
$config = Get-Content $MeshJson -Raw | ConvertFrom-Json
|
$config = Get-Content $MeshJson -Raw | ConvertFrom-Json
|
||||||
$squads = $config.squads.PSObject.Properties.Name
|
$squads = $config.squads.PSObject.Properties.Name
|
||||||
|
|
||||||
# Create squad directories with placeholder SUMMARY.md
|
# Create squad directories with placeholder SUMMARY.md
|
||||||
foreach ($squad in $squads) {
|
foreach ($squad in $squads) {
|
||||||
if (-not (Test-Path $squad)) {
|
if (-not (Test-Path $squad)) {
|
||||||
New-Item -ItemType Directory -Path $squad | Out-Null
|
New-Item -ItemType Directory -Path $squad | Out-Null
|
||||||
Write-Host " ✓ Created $squad/"
|
Write-Host " ✓ Created $squad/"
|
||||||
} else {
|
} else {
|
||||||
Write-Host " • $squad/ exists (skipped)"
|
Write-Host " • $squad/ exists (skipped)"
|
||||||
}
|
}
|
||||||
|
|
||||||
$summaryPath = "$squad/SUMMARY.md"
|
$summaryPath = "$squad/SUMMARY.md"
|
||||||
if (-not (Test-Path $summaryPath)) {
|
if (-not (Test-Path $summaryPath)) {
|
||||||
"# $squad`n`n_No state published yet._" | Set-Content $summaryPath
|
"# $squad`n`n_No state published yet._" | Set-Content $summaryPath
|
||||||
Write-Host " ✓ Created $summaryPath"
|
Write-Host " ✓ Created $summaryPath"
|
||||||
} else {
|
} else {
|
||||||
Write-Host " • $summaryPath exists (skipped)"
|
Write-Host " • $summaryPath exists (skipped)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Generate root README.md
|
# Generate root README.md
|
||||||
if (-not (Test-Path "README.md")) {
|
if (-not (Test-Path "README.md")) {
|
||||||
$readme = @"
|
$readme = @"
|
||||||
# Squad Mesh State Repository
|
# Squad Mesh State Repository
|
||||||
|
|
||||||
This repository tracks published state from participating squads.
|
This repository tracks published state from participating squads.
|
||||||
|
|
||||||
## Participating Squads
|
## Participating Squads
|
||||||
|
|
||||||
"@
|
"@
|
||||||
foreach ($squad in $squads) {
|
foreach ($squad in $squads) {
|
||||||
$zone = $config.squads.$squad.zone
|
$zone = $config.squads.$squad.zone
|
||||||
$readme += "- **$squad** (Zone: $zone)`n"
|
$readme += "- **$squad** (Zone: $zone)`n"
|
||||||
}
|
}
|
||||||
$readme += @"
|
$readme += @"
|
||||||
|
|
||||||
Each squad directory contains a ``SUMMARY.md`` with their latest published state.
|
Each squad directory contains a ``SUMMARY.md`` with their latest published state.
|
||||||
State is synchronized using ``sync-mesh.sh`` or ``sync-mesh.ps1``.
|
State is synchronized using ``sync-mesh.sh`` or ``sync-mesh.ps1``.
|
||||||
"@
|
"@
|
||||||
$readme | Set-Content "README.md"
|
$readme | Set-Content "README.md"
|
||||||
Write-Host " ✓ Created README.md"
|
Write-Host " ✓ Created README.md"
|
||||||
} else {
|
} else {
|
||||||
Write-Host " • README.md exists (skipped)"
|
Write-Host " • README.md exists (skipped)"
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "✅ Mesh state repository initialized"
|
Write-Host "✅ Mesh state repository initialized"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
$config = Get-Content $MeshJson -Raw | ConvertFrom-Json
|
$config = Get-Content $MeshJson -Raw | ConvertFrom-Json
|
||||||
|
|
||||||
# Zone 2: Remote-trusted — git clone/pull
|
# Zone 2: Remote-trusted — git clone/pull
|
||||||
foreach ($entry in $config.squads.PSObject.Properties | Where-Object { $_.Value.zone -eq "remote-trusted" }) {
|
foreach ($entry in $config.squads.PSObject.Properties | Where-Object { $_.Value.zone -eq "remote-trusted" }) {
|
||||||
$squad = $entry.Name
|
$squad = $entry.Name
|
||||||
$source = $entry.Value.source
|
$source = $entry.Value.source
|
||||||
$ref = if ($entry.Value.ref) { $entry.Value.ref } else { "main" }
|
$ref = if ($entry.Value.ref) { $entry.Value.ref } else { "main" }
|
||||||
$target = $entry.Value.sync_to
|
$target = $entry.Value.sync_to
|
||||||
|
|
||||||
if (Test-Path "$target/.git") {
|
if (Test-Path "$target/.git") {
|
||||||
git -C $target pull --rebase --quiet 2>$null
|
git -C $target pull --rebase --quiet 2>$null
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Host "⚠ ${squad}: pull failed (using stale)" }
|
if ($LASTEXITCODE -ne 0) { Write-Host "⚠ ${squad}: pull failed (using stale)" }
|
||||||
} else {
|
} else {
|
||||||
New-Item -ItemType Directory -Force -Path (Split-Path $target -Parent) | Out-Null
|
New-Item -ItemType Directory -Force -Path (Split-Path $target -Parent) | Out-Null
|
||||||
git clone --quiet --depth 1 --branch $ref $source $target 2>$null
|
git clone --quiet --depth 1 --branch $ref $source $target 2>$null
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Host "⚠ ${squad}: clone failed (unavailable)" }
|
if ($LASTEXITCODE -ne 0) { Write-Host "⚠ ${squad}: clone failed (unavailable)" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Zone 3: Remote-opaque — fetch published contracts
|
# Zone 3: Remote-opaque — fetch published contracts
|
||||||
foreach ($entry in $config.squads.PSObject.Properties | Where-Object { $_.Value.zone -eq "remote-opaque" }) {
|
foreach ($entry in $config.squads.PSObject.Properties | Where-Object { $_.Value.zone -eq "remote-opaque" }) {
|
||||||
$squad = $entry.Name
|
$squad = $entry.Name
|
||||||
$source = $entry.Value.source
|
$source = $entry.Value.source
|
||||||
$target = $entry.Value.sync_to
|
$target = $entry.Value.sync_to
|
||||||
$auth = $entry.Value.auth
|
$auth = $entry.Value.auth
|
||||||
|
|
||||||
New-Item -ItemType Directory -Force -Path $target | Out-Null
|
New-Item -ItemType Directory -Force -Path $target | Out-Null
|
||||||
$params = @{ Uri = $source; OutFile = "$target/SUMMARY.md"; UseBasicParsing = $true }
|
$params = @{ Uri = $source; OutFile = "$target/SUMMARY.md"; UseBasicParsing = $true }
|
||||||
if ($auth -eq "bearer") {
|
if ($auth -eq "bearer") {
|
||||||
$tokenVar = ($squad.ToUpper() -replace '-', '_') + "_TOKEN"
|
$tokenVar = ($squad.ToUpper() -replace '-', '_') + "_TOKEN"
|
||||||
$token = [Environment]::GetEnvironmentVariable($tokenVar)
|
$token = [Environment]::GetEnvironmentVariable($tokenVar)
|
||||||
if ($token) { $params.Headers = @{ Authorization = "Bearer $token" } }
|
if ($token) { $params.Headers = @{ Authorization = "Bearer $token" } }
|
||||||
}
|
}
|
||||||
try { Invoke-WebRequest @params -ErrorAction Stop }
|
try { Invoke-WebRequest @params -ErrorAction Stop }
|
||||||
catch { "# ${squad} — unavailable ($(Get-Date))" | Set-Content "$target/SUMMARY.md" }
|
catch { "# ${squad} — unavailable ($(Get-Date))" | Set-Content "$target/SUMMARY.md" }
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "✓ Mesh sync complete"
|
Write-Host "✓ Mesh sync complete"
|
||||||
|
|||||||
@@ -1,104 +1,104 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# sync-mesh.sh — Materialize remote squad state locally
|
# sync-mesh.sh — Materialize remote squad state locally
|
||||||
#
|
#
|
||||||
# Reads mesh.json, fetches remote squads into local directories.
|
# Reads mesh.json, fetches remote squads into local directories.
|
||||||
# Run before agent reads. No daemon. No service. ~40 lines.
|
# Run before agent reads. No daemon. No service. ~40 lines.
|
||||||
#
|
#
|
||||||
# Usage: ./sync-mesh.sh [path-to-mesh.json]
|
# Usage: ./sync-mesh.sh [path-to-mesh.json]
|
||||||
# ./sync-mesh.sh --init [path-to-mesh.json]
|
# ./sync-mesh.sh --init [path-to-mesh.json]
|
||||||
# Requires: jq (https://github.com/jqlang/jq), git, curl
|
# Requires: jq (https://github.com/jqlang/jq), git, curl
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Handle --init mode
|
# Handle --init mode
|
||||||
if [ "${1:-}" = "--init" ]; then
|
if [ "${1:-}" = "--init" ]; then
|
||||||
MESH_JSON="${2:-mesh.json}"
|
MESH_JSON="${2:-mesh.json}"
|
||||||
|
|
||||||
if [ ! -f "$MESH_JSON" ]; then
|
if [ ! -f "$MESH_JSON" ]; then
|
||||||
echo "❌ $MESH_JSON not found"
|
echo "❌ $MESH_JSON not found"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "🚀 Initializing mesh state repository..."
|
echo "🚀 Initializing mesh state repository..."
|
||||||
squads=$(jq -r '.squads | keys[]' "$MESH_JSON")
|
squads=$(jq -r '.squads | keys[]' "$MESH_JSON")
|
||||||
|
|
||||||
# Create squad directories with placeholder SUMMARY.md
|
# Create squad directories with placeholder SUMMARY.md
|
||||||
for squad in $squads; do
|
for squad in $squads; do
|
||||||
if [ ! -d "$squad" ]; then
|
if [ ! -d "$squad" ]; then
|
||||||
mkdir -p "$squad"
|
mkdir -p "$squad"
|
||||||
echo " ✓ Created $squad/"
|
echo " ✓ Created $squad/"
|
||||||
else
|
else
|
||||||
echo " • $squad/ exists (skipped)"
|
echo " • $squad/ exists (skipped)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "$squad/SUMMARY.md" ]; then
|
if [ ! -f "$squad/SUMMARY.md" ]; then
|
||||||
echo -e "# $squad\n\n_No state published yet._" > "$squad/SUMMARY.md"
|
echo -e "# $squad\n\n_No state published yet._" > "$squad/SUMMARY.md"
|
||||||
echo " ✓ Created $squad/SUMMARY.md"
|
echo " ✓ Created $squad/SUMMARY.md"
|
||||||
else
|
else
|
||||||
echo " • $squad/SUMMARY.md exists (skipped)"
|
echo " • $squad/SUMMARY.md exists (skipped)"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Generate root README.md
|
# Generate root README.md
|
||||||
if [ ! -f "README.md" ]; then
|
if [ ! -f "README.md" ]; then
|
||||||
{
|
{
|
||||||
echo "# Squad Mesh State Repository"
|
echo "# Squad Mesh State Repository"
|
||||||
echo ""
|
echo ""
|
||||||
echo "This repository tracks published state from participating squads."
|
echo "This repository tracks published state from participating squads."
|
||||||
echo ""
|
echo ""
|
||||||
echo "## Participating Squads"
|
echo "## Participating Squads"
|
||||||
echo ""
|
echo ""
|
||||||
for squad in $squads; do
|
for squad in $squads; do
|
||||||
zone=$(jq -r ".squads.\"$squad\".zone" "$MESH_JSON")
|
zone=$(jq -r ".squads.\"$squad\".zone" "$MESH_JSON")
|
||||||
echo "- **$squad** (Zone: $zone)"
|
echo "- **$squad** (Zone: $zone)"
|
||||||
done
|
done
|
||||||
echo ""
|
echo ""
|
||||||
echo "Each squad directory contains a \`SUMMARY.md\` with their latest published state."
|
echo "Each squad directory contains a \`SUMMARY.md\` with their latest published state."
|
||||||
echo "State is synchronized using \`sync-mesh.sh\` or \`sync-mesh.ps1\`."
|
echo "State is synchronized using \`sync-mesh.sh\` or \`sync-mesh.ps1\`."
|
||||||
} > README.md
|
} > README.md
|
||||||
echo " ✓ Created README.md"
|
echo " ✓ Created README.md"
|
||||||
else
|
else
|
||||||
echo " • README.md exists (skipped)"
|
echo " • README.md exists (skipped)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ Mesh state repository initialized"
|
echo "✅ Mesh state repository initialized"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
MESH_JSON="${1:-mesh.json}"
|
MESH_JSON="${1:-mesh.json}"
|
||||||
|
|
||||||
# Zone 2: Remote-trusted — git clone/pull
|
# Zone 2: Remote-trusted — git clone/pull
|
||||||
for squad in $(jq -r '.squads | to_entries[] | select(.value.zone == "remote-trusted") | .key' "$MESH_JSON"); do
|
for squad in $(jq -r '.squads | to_entries[] | select(.value.zone == "remote-trusted") | .key' "$MESH_JSON"); do
|
||||||
source=$(jq -r ".squads.\"$squad\".source" "$MESH_JSON")
|
source=$(jq -r ".squads.\"$squad\".source" "$MESH_JSON")
|
||||||
ref=$(jq -r ".squads.\"$squad\".ref // \"main\"" "$MESH_JSON")
|
ref=$(jq -r ".squads.\"$squad\".ref // \"main\"" "$MESH_JSON")
|
||||||
target=$(jq -r ".squads.\"$squad\".sync_to" "$MESH_JSON")
|
target=$(jq -r ".squads.\"$squad\".sync_to" "$MESH_JSON")
|
||||||
|
|
||||||
if [ -d "$target/.git" ]; then
|
if [ -d "$target/.git" ]; then
|
||||||
git -C "$target" pull --rebase --quiet 2>/dev/null \
|
git -C "$target" pull --rebase --quiet 2>/dev/null \
|
||||||
|| echo "⚠ $squad: pull failed (using stale)"
|
|| echo "⚠ $squad: pull failed (using stale)"
|
||||||
else
|
else
|
||||||
mkdir -p "$(dirname "$target")"
|
mkdir -p "$(dirname "$target")"
|
||||||
git clone --quiet --depth 1 --branch "$ref" "$source" "$target" 2>/dev/null \
|
git clone --quiet --depth 1 --branch "$ref" "$source" "$target" 2>/dev/null \
|
||||||
|| echo "⚠ $squad: clone failed (unavailable)"
|
|| echo "⚠ $squad: clone failed (unavailable)"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Zone 3: Remote-opaque — fetch published contracts
|
# Zone 3: Remote-opaque — fetch published contracts
|
||||||
for squad in $(jq -r '.squads | to_entries[] | select(.value.zone == "remote-opaque") | .key' "$MESH_JSON"); do
|
for squad in $(jq -r '.squads | to_entries[] | select(.value.zone == "remote-opaque") | .key' "$MESH_JSON"); do
|
||||||
source=$(jq -r ".squads.\"$squad\".source" "$MESH_JSON")
|
source=$(jq -r ".squads.\"$squad\".source" "$MESH_JSON")
|
||||||
target=$(jq -r ".squads.\"$squad\".sync_to" "$MESH_JSON")
|
target=$(jq -r ".squads.\"$squad\".sync_to" "$MESH_JSON")
|
||||||
auth=$(jq -r ".squads.\"$squad\".auth // \"\"" "$MESH_JSON")
|
auth=$(jq -r ".squads.\"$squad\".auth // \"\"" "$MESH_JSON")
|
||||||
|
|
||||||
mkdir -p "$target"
|
mkdir -p "$target"
|
||||||
auth_flag=""
|
auth_flag=""
|
||||||
if [ "$auth" = "bearer" ]; then
|
if [ "$auth" = "bearer" ]; then
|
||||||
token_var="$(echo "${squad}" | tr '[:lower:]-' '[:upper:]_')_TOKEN"
|
token_var="$(echo "${squad}" | tr '[:lower:]-' '[:upper:]_')_TOKEN"
|
||||||
[ -n "${!token_var:-}" ] && auth_flag="--header \"Authorization: Bearer ${!token_var}\""
|
[ -n "${!token_var:-}" ] && auth_flag="--header \"Authorization: Bearer ${!token_var}\""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
eval curl --silent --fail $auth_flag "$source" -o "$target/SUMMARY.md" 2>/dev/null \
|
eval curl --silent --fail $auth_flag "$source" -o "$target/SUMMARY.md" 2>/dev/null \
|
||||||
|| echo "# ${squad} — unavailable ($(date))" > "$target/SUMMARY.md"
|
|| echo "# ${squad} — unavailable ($(date))" > "$target/SUMMARY.md"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "✓ Mesh sync complete"
|
echo "✓ Mesh sync complete"
|
||||||
|
|||||||
@@ -1,71 +1,71 @@
|
|||||||
---
|
---
|
||||||
name: "docs-standards"
|
name: "docs-standards"
|
||||||
description: "Microsoft Style Guide + Squad-specific documentation patterns"
|
description: "Microsoft Style Guide + Squad-specific documentation patterns"
|
||||||
domain: "documentation"
|
domain: "documentation"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "earned (PAO charter, multiple doc PR reviews)"
|
source: "earned (PAO charter, multiple doc PR reviews)"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Squad documentation follows the Microsoft Style Guide with Squad-specific conventions. Consistency across docs builds trust and improves discoverability.
|
Squad documentation follows the Microsoft Style Guide with Squad-specific conventions. Consistency across docs builds trust and improves discoverability.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### Microsoft Style Guide Rules
|
### Microsoft Style Guide Rules
|
||||||
- **Sentence-case headings:** "Getting started" not "Getting Started"
|
- **Sentence-case headings:** "Getting started" not "Getting Started"
|
||||||
- **Active voice:** "Run the command" not "The command should be run"
|
- **Active voice:** "Run the command" not "The command should be run"
|
||||||
- **Second person:** "You can configure..." not "Users can configure..."
|
- **Second person:** "You can configure..." not "Users can configure..."
|
||||||
- **Present tense:** "The system routes..." not "The system will route..."
|
- **Present tense:** "The system routes..." not "The system will route..."
|
||||||
- **No ampersands in prose:** "and" not "&" (except in code, brand names, or UI elements)
|
- **No ampersands in prose:** "and" not "&" (except in code, brand names, or UI elements)
|
||||||
|
|
||||||
### Squad Formatting Patterns
|
### Squad Formatting Patterns
|
||||||
- **Scannability first:** Paragraphs for narrative (3-4 sentences max), bullets for scannable lists, tables for structured data
|
- **Scannability first:** Paragraphs for narrative (3-4 sentences max), bullets for scannable lists, tables for structured data
|
||||||
- **"Try this" prompts at top:** Start feature/scenario pages with practical prompts users can copy
|
- **"Try this" prompts at top:** Start feature/scenario pages with practical prompts users can copy
|
||||||
- **Experimental warnings:** Features in preview get callout at top
|
- **Experimental warnings:** Features in preview get callout at top
|
||||||
- **Cross-references at bottom:** Related pages linked after main content
|
- **Cross-references at bottom:** Related pages linked after main content
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
- **Title (H1)** → **Warning/callout** → **Try this code** → **Overview** → **HR** → **Content (H2 sections)**
|
- **Title (H1)** → **Warning/callout** → **Try this code** → **Overview** → **HR** → **Content (H2 sections)**
|
||||||
|
|
||||||
### Test Sync Rule
|
### Test Sync Rule
|
||||||
- **Always update test assertions:** When adding docs pages to `features/`, `scenarios/`, `guides/`, update corresponding `EXPECTED_*` arrays in `test/docs-build.test.ts` in the same commit
|
- **Always update test assertions:** When adding docs pages to `features/`, `scenarios/`, `guides/`, update corresponding `EXPECTED_*` arrays in `test/docs-build.test.ts` in the same commit
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
✓ **Correct:**
|
✓ **Correct:**
|
||||||
```markdown
|
```markdown
|
||||||
# Getting started with Squad
|
# Getting started with Squad
|
||||||
|
|
||||||
> ⚠️ **Experimental:** This feature is in preview.
|
> ⚠️ **Experimental:** This feature is in preview.
|
||||||
|
|
||||||
Try this:
|
Try this:
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
squad init
|
squad init
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Squad helps you build AI teams...
|
Squad helps you build AI teams...
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Install Squad
|
## Install Squad
|
||||||
|
|
||||||
Run the following command...
|
Run the following command...
|
||||||
```
|
```
|
||||||
|
|
||||||
✗ **Incorrect:**
|
✗ **Incorrect:**
|
||||||
```markdown
|
```markdown
|
||||||
# Getting Started With Squad // Title case
|
# Getting Started With Squad // Title case
|
||||||
|
|
||||||
Squad is a tool which will help users... // Third person, future tense
|
Squad is a tool which will help users... // Third person, future tense
|
||||||
|
|
||||||
You can install Squad with npm & configure it... // Ampersand in prose
|
You can install Squad with npm & configure it... // Ampersand in prose
|
||||||
```
|
```
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- Title-casing headings because "it looks nicer"
|
- Title-casing headings because "it looks nicer"
|
||||||
- Writing in passive voice or third person
|
- Writing in passive voice or third person
|
||||||
- Long paragraphs of dense text (breaks scannability)
|
- Long paragraphs of dense text (breaks scannability)
|
||||||
- Adding doc pages without updating test assertions
|
- Adding doc pages without updating test assertions
|
||||||
- Using ampersands outside code blocks
|
- Using ampersands outside code blocks
|
||||||
|
|||||||
@@ -1,114 +1,114 @@
|
|||||||
---
|
---
|
||||||
name: "economy-mode"
|
name: "economy-mode"
|
||||||
description: "Shifts Layer 3 model selection to cost-optimized alternatives when economy mode is active."
|
description: "Shifts Layer 3 model selection to cost-optimized alternatives when economy mode is active."
|
||||||
domain: "model-selection"
|
domain: "model-selection"
|
||||||
confidence: "low"
|
confidence: "low"
|
||||||
source: "manual"
|
source: "manual"
|
||||||
---
|
---
|
||||||
|
|
||||||
## SCOPE
|
## SCOPE
|
||||||
|
|
||||||
✅ THIS SKILL PRODUCES:
|
✅ THIS SKILL PRODUCES:
|
||||||
- A modified Layer 3 model selection table applied when economy mode is active
|
- A modified Layer 3 model selection table applied when economy mode is active
|
||||||
- `economyMode: true` written to `.squad/config.json` when activated persistently
|
- `economyMode: true` written to `.squad/config.json` when activated persistently
|
||||||
- Spawn acknowledgments with `💰` indicator when economy mode is active
|
- Spawn acknowledgments with `💰` indicator when economy mode is active
|
||||||
|
|
||||||
❌ THIS SKILL DOES NOT PRODUCE:
|
❌ THIS SKILL DOES NOT PRODUCE:
|
||||||
- Code, tests, or documentation
|
- Code, tests, or documentation
|
||||||
- Cost reports or billing artifacts
|
- Cost reports or billing artifacts
|
||||||
- Changes to Layer 0, Layer 1, or Layer 2 resolution (user intent always wins)
|
- Changes to Layer 0, Layer 1, or Layer 2 resolution (user intent always wins)
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Economy mode shifts Layer 3 (Task-Aware Auto-Selection) to lower-cost alternatives. It does NOT override persistent config (`defaultModel`, `agentModelOverrides`) or per-agent charter preferences — those represent explicit user intent and always take priority.
|
Economy mode shifts Layer 3 (Task-Aware Auto-Selection) to lower-cost alternatives. It does NOT override persistent config (`defaultModel`, `agentModelOverrides`) or per-agent charter preferences — those represent explicit user intent and always take priority.
|
||||||
|
|
||||||
Use this skill when the user wants to reduce costs across an entire session or permanently, without manually specifying models for each agent.
|
Use this skill when the user wants to reduce costs across an entire session or permanently, without manually specifying models for each agent.
|
||||||
|
|
||||||
## Activation Methods
|
## Activation Methods
|
||||||
|
|
||||||
| Method | How |
|
| Method | How |
|
||||||
|--------|-----|
|
|--------|-----|
|
||||||
| Session phrase | "use economy mode", "save costs", "go cheap", "reduce costs" |
|
| Session phrase | "use economy mode", "save costs", "go cheap", "reduce costs" |
|
||||||
| Persistent config | `"economyMode": true` in `.squad/config.json` |
|
| Persistent config | `"economyMode": true` in `.squad/config.json` |
|
||||||
| CLI flag | `squad --economy` |
|
| CLI flag | `squad --economy` |
|
||||||
|
|
||||||
**Deactivation:** "turn off economy mode", "disable economy mode", or remove `economyMode` from `config.json`.
|
**Deactivation:** "turn off economy mode", "disable economy mode", or remove `economyMode` from `config.json`.
|
||||||
|
|
||||||
## Economy Model Selection Table
|
## Economy Model Selection Table
|
||||||
|
|
||||||
When economy mode is **active**, Layer 3 auto-selection uses this table instead of the normal defaults:
|
When economy mode is **active**, Layer 3 auto-selection uses this table instead of the normal defaults:
|
||||||
|
|
||||||
| Task Output | Normal Mode | Economy Mode |
|
| Task Output | Normal Mode | Economy Mode |
|
||||||
|-------------|-------------|--------------|
|
|-------------|-------------|--------------|
|
||||||
| Writing code (implementation, refactoring, bug fixes) | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` |
|
| Writing code (implementation, refactoring, bug fixes) | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` |
|
||||||
| Writing prompts or agent designs | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` |
|
| Writing prompts or agent designs | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` |
|
||||||
| Docs, planning, triage, changelogs, mechanical ops | `claude-haiku-4.5` | `gpt-4.1` or `gpt-5-mini` |
|
| Docs, planning, triage, changelogs, mechanical ops | `claude-haiku-4.5` | `gpt-4.1` or `gpt-5-mini` |
|
||||||
| Architecture, code review, security audits | `claude-opus-4.5` | `claude-sonnet-4.5` |
|
| Architecture, code review, security audits | `claude-opus-4.5` | `claude-sonnet-4.5` |
|
||||||
| Scribe / logger / mechanical file ops | `claude-haiku-4.5` | `gpt-4.1` |
|
| Scribe / logger / mechanical file ops | `claude-haiku-4.5` | `gpt-4.1` |
|
||||||
|
|
||||||
**Prefer `gpt-4.1` over `gpt-5-mini`** when the task involves structured output or agentic tool use. Prefer `gpt-5-mini` for pure text generation tasks where latency matters.
|
**Prefer `gpt-4.1` over `gpt-5-mini`** when the task involves structured output or agentic tool use. Prefer `gpt-5-mini` for pure text generation tasks where latency matters.
|
||||||
|
|
||||||
## AGENT WORKFLOW
|
## AGENT WORKFLOW
|
||||||
|
|
||||||
### On Session Start
|
### On Session Start
|
||||||
|
|
||||||
1. READ `.squad/config.json`
|
1. READ `.squad/config.json`
|
||||||
2. CHECK for `economyMode: true` — if present, activate economy mode for the session
|
2. CHECK for `economyMode: true` — if present, activate economy mode for the session
|
||||||
3. STORE economy mode state in session context
|
3. STORE economy mode state in session context
|
||||||
|
|
||||||
### On User Phrase Trigger
|
### On User Phrase Trigger
|
||||||
|
|
||||||
**Session-only (no config change):** "use economy mode", "save costs", "go cheap"
|
**Session-only (no config change):** "use economy mode", "save costs", "go cheap"
|
||||||
|
|
||||||
1. SET economy mode active for this session
|
1. SET economy mode active for this session
|
||||||
2. ACKNOWLEDGE: `✅ Economy mode active — using cost-optimized models this session. (Layer 0 and Layer 2 preferences still apply)`
|
2. ACKNOWLEDGE: `✅ Economy mode active — using cost-optimized models this session. (Layer 0 and Layer 2 preferences still apply)`
|
||||||
|
|
||||||
**Persistent:** "always use economy mode", "save economy mode"
|
**Persistent:** "always use economy mode", "save economy mode"
|
||||||
|
|
||||||
1. WRITE `economyMode: true` to `.squad/config.json` (merge, don't overwrite other fields)
|
1. WRITE `economyMode: true` to `.squad/config.json` (merge, don't overwrite other fields)
|
||||||
2. ACKNOWLEDGE: `✅ Economy mode saved — cost-optimized models will be used until disabled.`
|
2. ACKNOWLEDGE: `✅ Economy mode saved — cost-optimized models will be used until disabled.`
|
||||||
|
|
||||||
### On Every Agent Spawn (Economy Mode Active)
|
### On Every Agent Spawn (Economy Mode Active)
|
||||||
|
|
||||||
1. CHECK Layer 0a/0b first (agentModelOverrides, defaultModel) — if set, use that. Economy mode does NOT override Layer 0.
|
1. CHECK Layer 0a/0b first (agentModelOverrides, defaultModel) — if set, use that. Economy mode does NOT override Layer 0.
|
||||||
2. CHECK Layer 1 (session directive for a specific model) — if set, use that. Economy mode does NOT override explicit session directives.
|
2. CHECK Layer 1 (session directive for a specific model) — if set, use that. Economy mode does NOT override explicit session directives.
|
||||||
3. CHECK Layer 2 (charter preference) — if set, use that. Economy mode does NOT override charter preferences.
|
3. CHECK Layer 2 (charter preference) — if set, use that. Economy mode does NOT override charter preferences.
|
||||||
4. APPLY economy table at Layer 3 instead of normal table.
|
4. APPLY economy table at Layer 3 instead of normal table.
|
||||||
5. INCLUDE `💰` in spawn acknowledgment: `🔧 {Name} ({model} · 💰 economy) — {task}`
|
5. INCLUDE `💰` in spawn acknowledgment: `🔧 {Name} ({model} · 💰 economy) — {task}`
|
||||||
|
|
||||||
### On Deactivation
|
### On Deactivation
|
||||||
|
|
||||||
**Trigger phrases:** "turn off economy mode", "disable economy mode", "use normal models"
|
**Trigger phrases:** "turn off economy mode", "disable economy mode", "use normal models"
|
||||||
|
|
||||||
1. REMOVE `economyMode` from `.squad/config.json` (if it was persisted)
|
1. REMOVE `economyMode` from `.squad/config.json` (if it was persisted)
|
||||||
2. CLEAR session economy mode state
|
2. CLEAR session economy mode state
|
||||||
3. ACKNOWLEDGE: `✅ Economy mode disabled — returning to standard model selection.`
|
3. ACKNOWLEDGE: `✅ Economy mode disabled — returning to standard model selection.`
|
||||||
|
|
||||||
### STOP
|
### STOP
|
||||||
|
|
||||||
After updating economy mode state and including the `💰` indicator in spawn acknowledgments, this skill is done. Do NOT:
|
After updating economy mode state and including the `💰` indicator in spawn acknowledgments, this skill is done. Do NOT:
|
||||||
- Change Layer 0, Layer 1, or Layer 2 model choices
|
- Change Layer 0, Layer 1, or Layer 2 model choices
|
||||||
- Override charter-specified models
|
- Override charter-specified models
|
||||||
- Generate cost reports or comparisons
|
- Generate cost reports or comparisons
|
||||||
- Fall back to premium models via economy mode (economy mode never bumps UP)
|
- Fall back to premium models via economy mode (economy mode never bumps UP)
|
||||||
|
|
||||||
## Config Schema
|
## Config Schema
|
||||||
|
|
||||||
`.squad/config.json` economy-related fields:
|
`.squad/config.json` economy-related fields:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"economyMode": true
|
"economyMode": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `economyMode` — when `true`, Layer 3 uses the economy table. Optional; absent = economy mode off.
|
- `economyMode` — when `true`, Layer 3 uses the economy table. Optional; absent = economy mode off.
|
||||||
- Combines with `defaultModel` and `agentModelOverrides` — Layer 0 always wins.
|
- Combines with `defaultModel` and `agentModelOverrides` — Layer 0 always wins.
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- **Don't override Layer 0 in economy mode.** If the user set `defaultModel: "claude-opus-4.6"`, they want quality. Economy mode only affects Layer 3 auto-selection.
|
- **Don't override Layer 0 in economy mode.** If the user set `defaultModel: "claude-opus-4.6"`, they want quality. Economy mode only affects Layer 3 auto-selection.
|
||||||
- **Don't silently apply economy mode.** Always acknowledge when activated or deactivated.
|
- **Don't silently apply economy mode.** Always acknowledge when activated or deactivated.
|
||||||
- **Don't treat economy mode as permanent by default.** Session phrases activate session-only; only "always" or `config.json` persist it.
|
- **Don't treat economy mode as permanent by default.** Session phrases activate session-only; only "always" or `config.json` persist it.
|
||||||
- **Don't bump premium tasks down too far.** Architecture and security reviews shift from opus to sonnet in economy mode — they do NOT go to fast/cheap models.
|
- **Don't bump premium tasks down too far.** Architecture and security reviews shift from opus to sonnet in economy mode — they do NOT go to fast/cheap models.
|
||||||
|
|||||||
@@ -1,329 +1,329 @@
|
|||||||
---
|
---
|
||||||
name: "external-comms"
|
name: "external-comms"
|
||||||
description: "PAO workflow for scanning, drafting, and presenting community responses with human review gate"
|
description: "PAO workflow for scanning, drafting, and presenting community responses with human review gate"
|
||||||
domain: "community, communication, workflow"
|
domain: "community, communication, workflow"
|
||||||
confidence: "low"
|
confidence: "low"
|
||||||
source: "manual (RFC #426 — PAO External Communications)"
|
source: "manual (RFC #426 — PAO External Communications)"
|
||||||
tools:
|
tools:
|
||||||
- name: "github-mcp-server-list_issues"
|
- name: "github-mcp-server-list_issues"
|
||||||
description: "List open issues for scan candidates and lightweight triage"
|
description: "List open issues for scan candidates and lightweight triage"
|
||||||
when: "Use for recent open issue scans before thread-level review"
|
when: "Use for recent open issue scans before thread-level review"
|
||||||
- name: "github-mcp-server-issue_read"
|
- name: "github-mcp-server-issue_read"
|
||||||
description: "Read the full issue, comments, and labels before drafting"
|
description: "Read the full issue, comments, and labels before drafting"
|
||||||
when: "Use after selecting a candidate so PAO has complete thread context"
|
when: "Use after selecting a candidate so PAO has complete thread context"
|
||||||
- name: "github-mcp-server-search_issues"
|
- name: "github-mcp-server-search_issues"
|
||||||
description: "Search for candidate issues or prior squad responses"
|
description: "Search for candidate issues or prior squad responses"
|
||||||
when: "Use when filtering by keywords, labels, or duplicate response checks"
|
when: "Use when filtering by keywords, labels, or duplicate response checks"
|
||||||
- name: "gh CLI"
|
- name: "gh CLI"
|
||||||
description: "Fallback for GitHub issue comments and discussions workflows"
|
description: "Fallback for GitHub issue comments and discussions workflows"
|
||||||
when: "Use gh issue list/comment and gh api or gh api graphql when MCP coverage is incomplete"
|
when: "Use gh issue list/comment and gh api or gh api graphql when MCP coverage is incomplete"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Phase 1 is **draft-only mode**.
|
Phase 1 is **draft-only mode**.
|
||||||
|
|
||||||
- PAO scans issues and discussions, drafts responses with the humanizer skill, and presents a review table for human approval.
|
- PAO scans issues and discussions, drafts responses with the humanizer skill, and presents a review table for human approval.
|
||||||
- **Human review gate is mandatory** — PAO never posts autonomously.
|
- **Human review gate is mandatory** — PAO never posts autonomously.
|
||||||
- Every action is logged to `.squad/comms/audit/`.
|
- Every action is logged to `.squad/comms/audit/`.
|
||||||
- This workflow is triggered manually only ("PAO, check community") — no automated or Ralph-triggered activation in Phase 1.
|
- This workflow is triggered manually only ("PAO, check community") — no automated or Ralph-triggered activation in Phase 1.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### 1. Scan
|
### 1. Scan
|
||||||
|
|
||||||
Find unanswered community items with GitHub MCP tools first, or `gh issue list` / `gh api` as fallback for issues and discussions.
|
Find unanswered community items with GitHub MCP tools first, or `gh issue list` / `gh api` as fallback for issues and discussions.
|
||||||
|
|
||||||
- Include **open** issues and discussions only.
|
- Include **open** issues and discussions only.
|
||||||
- Filter for items with **no squad team response**.
|
- Filter for items with **no squad team response**.
|
||||||
- Limit to items created in the last 7 days.
|
- Limit to items created in the last 7 days.
|
||||||
- Exclude items labeled `squad:internal` or `wontfix`.
|
- Exclude items labeled `squad:internal` or `wontfix`.
|
||||||
- Include discussions **and** issues in the same sweep.
|
- Include discussions **and** issues in the same sweep.
|
||||||
- Phase 1 scope is **issues and discussions only** — do not draft PR replies.
|
- Phase 1 scope is **issues and discussions only** — do not draft PR replies.
|
||||||
|
|
||||||
### Discussion Handling (Phase 1)
|
### Discussion Handling (Phase 1)
|
||||||
|
|
||||||
Discussions use the GitHub Discussions API, which differs from issues:
|
Discussions use the GitHub Discussions API, which differs from issues:
|
||||||
|
|
||||||
- **Scan:** `gh api /repos/{owner}/{repo}/discussions --jq '.[] | select(.answer_chosen_at == null)'` to find unanswered discussions
|
- **Scan:** `gh api /repos/{owner}/{repo}/discussions --jq '.[] | select(.answer_chosen_at == null)'` to find unanswered discussions
|
||||||
- **Categories:** Filter by Q&A and General categories only (skip Announcements, Show and Tell)
|
- **Categories:** Filter by Q&A and General categories only (skip Announcements, Show and Tell)
|
||||||
- **Answers vs comments:** In Q&A discussions, PAO drafts an "answer" (not a comment). The human marks it as accepted answer after posting.
|
- **Answers vs comments:** In Q&A discussions, PAO drafts an "answer" (not a comment). The human marks it as accepted answer after posting.
|
||||||
- **Phase 1 scope:** Issues and Discussions ONLY. No PR comments.
|
- **Phase 1 scope:** Issues and Discussions ONLY. No PR comments.
|
||||||
|
|
||||||
### 2. Classify
|
### 2. Classify
|
||||||
|
|
||||||
Determine the response type before drafting.
|
Determine the response type before drafting.
|
||||||
|
|
||||||
- Welcome (new contributor)
|
- Welcome (new contributor)
|
||||||
- Troubleshooting (bug/help)
|
- Troubleshooting (bug/help)
|
||||||
- Feature guidance (feature request/how-to)
|
- Feature guidance (feature request/how-to)
|
||||||
- Redirect (wrong repo/scope)
|
- Redirect (wrong repo/scope)
|
||||||
- Acknowledgment (confirmed, no fix)
|
- Acknowledgment (confirmed, no fix)
|
||||||
- Closing (resolved)
|
- Closing (resolved)
|
||||||
- Technical uncertainty (unknown cause)
|
- Technical uncertainty (unknown cause)
|
||||||
- Empathetic disagreement (pushback on a decision or design)
|
- Empathetic disagreement (pushback on a decision or design)
|
||||||
- Information request (need more reproduction details or context)
|
- Information request (need more reproduction details or context)
|
||||||
|
|
||||||
### Template Selection Guide
|
### Template Selection Guide
|
||||||
|
|
||||||
| Signal in Issue/Discussion | → Response Type | Template |
|
| Signal in Issue/Discussion | → Response Type | Template |
|
||||||
|---------------------------|-----------------|----------|
|
|---------------------------|-----------------|----------|
|
||||||
| New contributor (0 prior issues) | Welcome | T1 |
|
| New contributor (0 prior issues) | Welcome | T1 |
|
||||||
| Error message, stack trace, "doesn't work" | Troubleshooting | T2 |
|
| Error message, stack trace, "doesn't work" | Troubleshooting | T2 |
|
||||||
| "How do I...?", "Can Squad...?", "Is there a way to...?" | Feature Guidance | T3 |
|
| "How do I...?", "Can Squad...?", "Is there a way to...?" | Feature Guidance | T3 |
|
||||||
| Wrong repo, out of scope for Squad | Redirect | T4 |
|
| Wrong repo, out of scope for Squad | Redirect | T4 |
|
||||||
| Confirmed bug, no fix available yet | Acknowledgment | T5 |
|
| Confirmed bug, no fix available yet | Acknowledgment | T5 |
|
||||||
| Fix shipped, PR merged that resolves issue | Closing | T6 |
|
| Fix shipped, PR merged that resolves issue | Closing | T6 |
|
||||||
| Unclear cause, needs investigation | Technical Uncertainty | T7 |
|
| Unclear cause, needs investigation | Technical Uncertainty | T7 |
|
||||||
| Author disagrees with a decision or design | Empathetic Disagreement | T8 |
|
| Author disagrees with a decision or design | Empathetic Disagreement | T8 |
|
||||||
| Need more reproduction info or context | Information Request | T9 |
|
| Need more reproduction info or context | Information Request | T9 |
|
||||||
|
|
||||||
Use exactly one template as the base draft. Replace placeholders with issue-specific details, then apply the humanizer patterns. If the thread spans multiple signals, choose the highest-risk template and capture the nuance in the thread summary.
|
Use exactly one template as the base draft. Replace placeholders with issue-specific details, then apply the humanizer patterns. If the thread spans multiple signals, choose the highest-risk template and capture the nuance in the thread summary.
|
||||||
|
|
||||||
### Confidence Classification
|
### Confidence Classification
|
||||||
|
|
||||||
| Confidence | Criteria | Example |
|
| Confidence | Criteria | Example |
|
||||||
|-----------|----------|---------|
|
|-----------|----------|---------|
|
||||||
| 🟢 High | Answer exists in Squad docs or FAQ, similar question answered before, no technical ambiguity | "How do I install Squad?" |
|
| 🟢 High | Answer exists in Squad docs or FAQ, similar question answered before, no technical ambiguity | "How do I install Squad?" |
|
||||||
| 🟡 Medium | Technical answer is sound but involves judgment calls, OR docs exist but don't perfectly match the question, OR tone is tricky | "Can Squad work with Azure DevOps?" (yes, but setup is nuanced) |
|
| 🟡 Medium | Technical answer is sound but involves judgment calls, OR docs exist but don't perfectly match the question, OR tone is tricky | "Can Squad work with Azure DevOps?" (yes, but setup is nuanced) |
|
||||||
| 🔴 Needs Review | Technical uncertainty, policy/roadmap question, potential reputational risk, author is frustrated/angry, question about unreleased features | "When will Squad support Claude?" |
|
| 🔴 Needs Review | Technical uncertainty, policy/roadmap question, potential reputational risk, author is frustrated/angry, question about unreleased features | "When will Squad support Claude?" |
|
||||||
|
|
||||||
**Auto-escalation rules:**
|
**Auto-escalation rules:**
|
||||||
- Any mention of competitors → 🔴
|
- Any mention of competitors → 🔴
|
||||||
- Any mention of pricing/licensing → 🔴
|
- Any mention of pricing/licensing → 🔴
|
||||||
- Author has >3 follow-up comments without resolution → 🔴
|
- Author has >3 follow-up comments without resolution → 🔴
|
||||||
- Question references a closed-wontfix issue → 🔴
|
- Question references a closed-wontfix issue → 🔴
|
||||||
|
|
||||||
### 3. Draft
|
### 3. Draft
|
||||||
|
|
||||||
Use the humanizer skill for every draft.
|
Use the humanizer skill for every draft.
|
||||||
|
|
||||||
- Complete **Thread-Read Verification** before writing.
|
- Complete **Thread-Read Verification** before writing.
|
||||||
- Read the **full thread**, including all comments, before writing.
|
- Read the **full thread**, including all comments, before writing.
|
||||||
- Select the matching template from the **Template Selection Guide** and record the template ID in the review notes.
|
- Select the matching template from the **Template Selection Guide** and record the template ID in the review notes.
|
||||||
- Treat templates as reusable drafting assets: keep the structure, replace placeholders, and only improvise when the thread truly requires it.
|
- Treat templates as reusable drafting assets: keep the structure, replace placeholders, and only improvise when the thread truly requires it.
|
||||||
- Validate the draft against the humanizer anti-patterns.
|
- Validate the draft against the humanizer anti-patterns.
|
||||||
- Flag long threads (`>10` comments) with `⚠️`.
|
- Flag long threads (`>10` comments) with `⚠️`.
|
||||||
|
|
||||||
### Thread-Read Verification
|
### Thread-Read Verification
|
||||||
|
|
||||||
Before drafting, PAO MUST verify complete thread coverage:
|
Before drafting, PAO MUST verify complete thread coverage:
|
||||||
|
|
||||||
1. **Count verification:** Compare API comment count with actually-read comments. If mismatch, abort draft.
|
1. **Count verification:** Compare API comment count with actually-read comments. If mismatch, abort draft.
|
||||||
2. **Deleted comment check:** Use `gh api` timeline to detect deleted comments. If found, flag as ⚠️ in review table.
|
2. **Deleted comment check:** Use `gh api` timeline to detect deleted comments. If found, flag as ⚠️ in review table.
|
||||||
3. **Thread summary:** Include in every draft: "Thread: {N} comments, last activity {date}, {summary of key points}"
|
3. **Thread summary:** Include in every draft: "Thread: {N} comments, last activity {date}, {summary of key points}"
|
||||||
4. **Long thread flag:** If >10 comments, add ⚠️ to review table and include condensed thread summary
|
4. **Long thread flag:** If >10 comments, add ⚠️ to review table and include condensed thread summary
|
||||||
5. **Evidence line in review table:** Each draft row includes "Read: {N}/{total} comments" column
|
5. **Evidence line in review table:** Each draft row includes "Read: {N}/{total} comments" column
|
||||||
|
|
||||||
### 4. Present
|
### 4. Present
|
||||||
|
|
||||||
Show drafts for review in this exact format:
|
Show drafts for review in this exact format:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
📝 PAO — Community Response Drafts
|
📝 PAO — Community Response Drafts
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
| # | Item | Author | Type | Confidence | Read | Preview |
|
| # | Item | Author | Type | Confidence | Read | Preview |
|
||||||
|---|------|--------|------|------------|------|---------|
|
|---|------|--------|------|------------|------|---------|
|
||||||
| 1 | Issue #N | @user | Type | 🟢/🟡/🔴 | N/N | "First words..." |
|
| 1 | Issue #N | @user | Type | 🟢/🟡/🔴 | N/N | "First words..." |
|
||||||
|
|
||||||
Confidence: 🟢 High | 🟡 Medium | 🔴 Needs review
|
Confidence: 🟢 High | 🟡 Medium | 🔴 Needs review
|
||||||
|
|
||||||
Full drafts below ▼
|
Full drafts below ▼
|
||||||
```
|
```
|
||||||
|
|
||||||
Each full draft must begin with the thread summary line:
|
Each full draft must begin with the thread summary line:
|
||||||
`Thread: {N} comments, last activity {date}, {summary of key points}`
|
`Thread: {N} comments, last activity {date}, {summary of key points}`
|
||||||
|
|
||||||
### 5. Human Action
|
### 5. Human Action
|
||||||
|
|
||||||
Wait for explicit human direction before anything is posted.
|
Wait for explicit human direction before anything is posted.
|
||||||
|
|
||||||
- `pao approve 1 3` — approve drafts 1 and 3
|
- `pao approve 1 3` — approve drafts 1 and 3
|
||||||
- `pao edit 2` — edit draft 2
|
- `pao edit 2` — edit draft 2
|
||||||
- `pao skip` — skip all
|
- `pao skip` — skip all
|
||||||
- `banana` — freeze all pending (safe word)
|
- `banana` — freeze all pending (safe word)
|
||||||
|
|
||||||
### Rollback — Bad Post Recovery
|
### Rollback — Bad Post Recovery
|
||||||
|
|
||||||
If a posted response turns out to be wrong, inappropriate, or needs correction:
|
If a posted response turns out to be wrong, inappropriate, or needs correction:
|
||||||
|
|
||||||
1. **Delete the comment:**
|
1. **Delete the comment:**
|
||||||
- Issues: `gh api -X DELETE /repos/{owner}/{repo}/issues/comments/{comment_id}`
|
- Issues: `gh api -X DELETE /repos/{owner}/{repo}/issues/comments/{comment_id}`
|
||||||
- Discussions: `gh api graphql -f query='mutation { deleteDiscussionComment(input: {id: "{node_id}"}) { comment { id } } }'`
|
- Discussions: `gh api graphql -f query='mutation { deleteDiscussionComment(input: {id: "{node_id}"}) { comment { id } } }'`
|
||||||
2. **Log the deletion:** Write audit entry with action `delete`, include reason and original content
|
2. **Log the deletion:** Write audit entry with action `delete`, include reason and original content
|
||||||
3. **Draft replacement** (if needed): PAO drafts a corrected response, goes through normal review cycle
|
3. **Draft replacement** (if needed): PAO drafts a corrected response, goes through normal review cycle
|
||||||
4. **Postmortem:** If the error reveals a pattern gap, update humanizer anti-patterns or add a new test case
|
4. **Postmortem:** If the error reveals a pattern gap, update humanizer anti-patterns or add a new test case
|
||||||
|
|
||||||
**Safe word — `banana`:**
|
**Safe word — `banana`:**
|
||||||
- Immediately freezes all pending drafts in the review queue
|
- Immediately freezes all pending drafts in the review queue
|
||||||
- No new scans or drafts until `pao resume` is issued
|
- No new scans or drafts until `pao resume` is issued
|
||||||
- Audit entry logged with halter identity and reason
|
- Audit entry logged with halter identity and reason
|
||||||
|
|
||||||
### 6. Post
|
### 6. Post
|
||||||
|
|
||||||
After approval:
|
After approval:
|
||||||
|
|
||||||
- Human posts via `gh issue comment` for issues or `gh api` for discussion answers/comments.
|
- Human posts via `gh issue comment` for issues or `gh api` for discussion answers/comments.
|
||||||
- PAO helps by preparing the CLI command.
|
- PAO helps by preparing the CLI command.
|
||||||
- Write the audit entry after the posting action.
|
- Write the audit entry after the posting action.
|
||||||
|
|
||||||
### 7. Audit
|
### 7. Audit
|
||||||
|
|
||||||
Log every action.
|
Log every action.
|
||||||
|
|
||||||
- Location: `.squad/comms/audit/{timestamp}.md`
|
- Location: `.squad/comms/audit/{timestamp}.md`
|
||||||
- Required fields vary by action — see `.squad/comms/templates/audit-entry.md` Conditional Fields table
|
- Required fields vary by action — see `.squad/comms/templates/audit-entry.md` Conditional Fields table
|
||||||
- Universal required fields: `timestamp`, `action`
|
- Universal required fields: `timestamp`, `action`
|
||||||
- All other fields are conditional on the action type
|
- All other fields are conditional on the action type
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
These are reusable templates. Keep the structure, replace placeholders, and adjust only where the thread requires it.
|
These are reusable templates. Keep the structure, replace placeholders, and adjust only where the thread requires it.
|
||||||
|
|
||||||
### Example scan command
|
### Example scan command
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gh issue list --state open --json number,title,author,labels,comments --limit 20
|
gh issue list --state open --json number,title,author,labels,comments --limit 20
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example review table
|
### Example review table
|
||||||
|
|
||||||
```text
|
```text
|
||||||
📝 PAO — Community Response Drafts
|
📝 PAO — Community Response Drafts
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
| # | Item | Author | Type | Confidence | Read | Preview |
|
| # | Item | Author | Type | Confidence | Read | Preview |
|
||||||
|---|------|--------|------|------------|------|---------|
|
|---|------|--------|------|------------|------|---------|
|
||||||
| 1 | Issue #426 | @newdev | Welcome | 🟢 | 1/1 | "Hey @newdev! Welcome to Squad..." |
|
| 1 | Issue #426 | @newdev | Welcome | 🟢 | 1/1 | "Hey @newdev! Welcome to Squad..." |
|
||||||
| 2 | Discussion #18 | @builder | Feature guidance | 🟡 | 4/4 | "Great question! Today the CLI..." |
|
| 2 | Discussion #18 | @builder | Feature guidance | 🟡 | 4/4 | "Great question! Today the CLI..." |
|
||||||
| 3 | Issue #431 ⚠️ | @debugger | Technical uncertainty | 🔴 | 12/12 | "Interesting find, @debugger..." |
|
| 3 | Issue #431 ⚠️ | @debugger | Technical uncertainty | 🔴 | 12/12 | "Interesting find, @debugger..." |
|
||||||
|
|
||||||
Confidence: 🟢 High | 🟡 Medium | 🔴 Needs review
|
Confidence: 🟢 High | 🟡 Medium | 🔴 Needs review
|
||||||
|
|
||||||
Full drafts below ▼
|
Full drafts below ▼
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example audit entry (post action)
|
### Example audit entry (post action)
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
---
|
---
|
||||||
timestamp: "2026-03-16T21:30:00Z"
|
timestamp: "2026-03-16T21:30:00Z"
|
||||||
action: "post"
|
action: "post"
|
||||||
item_number: 426
|
item_number: 426
|
||||||
draft_id: 1
|
draft_id: 1
|
||||||
reviewer: "@bradygaster"
|
reviewer: "@bradygaster"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context (draft, approve, edit, skip, post, delete actions)
|
## Context (draft, approve, edit, skip, post, delete actions)
|
||||||
- Thread depth: 3
|
- Thread depth: 3
|
||||||
- Response type: welcome
|
- Response type: welcome
|
||||||
- Confidence: 🟢
|
- Confidence: 🟢
|
||||||
- Long thread flag: false
|
- Long thread flag: false
|
||||||
|
|
||||||
## Draft Content (draft, edit, post actions)
|
## Draft Content (draft, edit, post actions)
|
||||||
Thread: 3 comments, last activity 2026-03-16, reporter hit a preview-build regression after install.
|
Thread: 3 comments, last activity 2026-03-16, reporter hit a preview-build regression after install.
|
||||||
|
|
||||||
Hey @newdev! Welcome to Squad 👋 Thanks for opening this.
|
Hey @newdev! Welcome to Squad 👋 Thanks for opening this.
|
||||||
We reproduced the issue in preview builds and we're checking the regression point now.
|
We reproduced the issue in preview builds and we're checking the regression point now.
|
||||||
Let us know if you can share the command you ran right before the failure.
|
Let us know if you can share the command you ran right before the failure.
|
||||||
|
|
||||||
## Post Result (post, delete actions)
|
## Post Result (post, delete actions)
|
||||||
https://github.com/bradygaster/squad/issues/426#issuecomment-123456
|
https://github.com/bradygaster/squad/issues/426#issuecomment-123456
|
||||||
```
|
```
|
||||||
|
|
||||||
### T1 — Welcome
|
### T1 — Welcome
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Hey {author}! Welcome to Squad 👋 Thanks for opening this.
|
Hey {author}! Welcome to Squad 👋 Thanks for opening this.
|
||||||
{specific acknowledgment or first answer}
|
{specific acknowledgment or first answer}
|
||||||
Let us know if you have questions — happy to help!
|
Let us know if you have questions — happy to help!
|
||||||
```
|
```
|
||||||
|
|
||||||
### T2 — Troubleshooting
|
### T2 — Troubleshooting
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Thanks for the detailed report, {author}!
|
Thanks for the detailed report, {author}!
|
||||||
Here's what we think is happening: {explanation}
|
Here's what we think is happening: {explanation}
|
||||||
{steps or workaround}
|
{steps or workaround}
|
||||||
Let us know if that helps, or if you're seeing something different.
|
Let us know if that helps, or if you're seeing something different.
|
||||||
```
|
```
|
||||||
|
|
||||||
### T3 — Feature Guidance
|
### T3 — Feature Guidance
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Great question! {context on current state}
|
Great question! {context on current state}
|
||||||
{guidance or workaround}
|
{guidance or workaround}
|
||||||
We've noted this as a potential improvement — {tracking info if applicable}.
|
We've noted this as a potential improvement — {tracking info if applicable}.
|
||||||
```
|
```
|
||||||
|
|
||||||
### T4 — Redirect
|
### T4 — Redirect
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Thanks for reaching out! This one is actually better suited for {correct location}.
|
Thanks for reaching out! This one is actually better suited for {correct location}.
|
||||||
{brief explanation of why}
|
{brief explanation of why}
|
||||||
Feel free to open it there — they'll be able to help!
|
Feel free to open it there — they'll be able to help!
|
||||||
```
|
```
|
||||||
|
|
||||||
### T5 — Acknowledgment
|
### T5 — Acknowledgment
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Good catch, {author}. We've confirmed this is a real issue.
|
Good catch, {author}. We've confirmed this is a real issue.
|
||||||
{what we know so far}
|
{what we know so far}
|
||||||
We'll update this thread when we have a fix. Thanks for flagging it!
|
We'll update this thread when we have a fix. Thanks for flagging it!
|
||||||
```
|
```
|
||||||
|
|
||||||
### T6 — Closing
|
### T6 — Closing
|
||||||
|
|
||||||
```text
|
```text
|
||||||
This should be resolved in {version/PR}! 🎉
|
This should be resolved in {version/PR}! 🎉
|
||||||
{brief summary of what changed}
|
{brief summary of what changed}
|
||||||
Thanks for reporting this, {author} — it made Squad better.
|
Thanks for reporting this, {author} — it made Squad better.
|
||||||
```
|
```
|
||||||
|
|
||||||
### T7 — Technical Uncertainty
|
### T7 — Technical Uncertainty
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Interesting find, {author}. We're not 100% sure what's causing this yet.
|
Interesting find, {author}. We're not 100% sure what's causing this yet.
|
||||||
Here's what we've ruled out: {list}
|
Here's what we've ruled out: {list}
|
||||||
We'd love more context if you have it — {specific ask}.
|
We'd love more context if you have it — {specific ask}.
|
||||||
We'll dig deeper and update this thread.
|
We'll dig deeper and update this thread.
|
||||||
```
|
```
|
||||||
|
|
||||||
### T8 — Empathetic Disagreement
|
### T8 — Empathetic Disagreement
|
||||||
|
|
||||||
```text
|
```text
|
||||||
We hear you, {author}. That's a fair concern.
|
We hear you, {author}. That's a fair concern.
|
||||||
|
|
||||||
The current design choice was driven by {reason}. We know it's not ideal for every use case.
|
The current design choice was driven by {reason}. We know it's not ideal for every use case.
|
||||||
|
|
||||||
{what alternatives exist or what trade-off was made}
|
{what alternatives exist or what trade-off was made}
|
||||||
|
|
||||||
If you have ideas for how to make this work better for your scenario, we'd love to hear them — open a discussion or drop your thoughts here!
|
If you have ideas for how to make this work better for your scenario, we'd love to hear them — open a discussion or drop your thoughts here!
|
||||||
```
|
```
|
||||||
|
|
||||||
### T9 — Information Request
|
### T9 — Information Request
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Thanks for reporting this, {author}!
|
Thanks for reporting this, {author}!
|
||||||
|
|
||||||
To help us dig into this, could you share:
|
To help us dig into this, could you share:
|
||||||
- {specific ask 1}
|
- {specific ask 1}
|
||||||
- {specific ask 2}
|
- {specific ask 2}
|
||||||
- {specific ask 3, if applicable}
|
- {specific ask 3, if applicable}
|
||||||
|
|
||||||
That context will help us narrow down what's happening. Appreciate it!
|
That context will help us narrow down what's happening. Appreciate it!
|
||||||
```
|
```
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- ❌ Posting without human review (NEVER — this is the cardinal rule)
|
- ❌ Posting without human review (NEVER — this is the cardinal rule)
|
||||||
- ❌ Drafting without reading full thread (context is everything)
|
- ❌ Drafting without reading full thread (context is everything)
|
||||||
- ❌ Ignoring confidence flags (🔴 items need Flight/human review)
|
- ❌ Ignoring confidence flags (🔴 items need Flight/human review)
|
||||||
- ❌ Scanning closed issues (only open items)
|
- ❌ Scanning closed issues (only open items)
|
||||||
- ❌ Responding to issues labeled `squad:internal` or `wontfix`
|
- ❌ Responding to issues labeled `squad:internal` or `wontfix`
|
||||||
- ❌ Skipping audit logging (every action must be recorded)
|
- ❌ Skipping audit logging (every action must be recorded)
|
||||||
- ❌ Drafting for issues where a squad member already responded (avoid duplicates)
|
- ❌ Drafting for issues where a squad member already responded (avoid duplicates)
|
||||||
- ❌ Drafting pull request responses in Phase 1 (issues/discussions only)
|
- ❌ Drafting pull request responses in Phase 1 (issues/discussions only)
|
||||||
- ❌ Treating templates like loose examples instead of reusable drafting assets
|
- ❌ Treating templates like loose examples instead of reusable drafting assets
|
||||||
- ❌ Asking for more info without specific requests
|
- ❌ Asking for more info without specific requests
|
||||||
|
|||||||
@@ -1,183 +1,183 @@
|
|||||||
---
|
---
|
||||||
name: "gh-auth-isolation"
|
name: "gh-auth-isolation"
|
||||||
description: "Safely manage multiple GitHub identities (EMU + personal) in agent workflows"
|
description: "Safely manage multiple GitHub identities (EMU + personal) in agent workflows"
|
||||||
domain: "security, github-integration, authentication, multi-account"
|
domain: "security, github-integration, authentication, multi-account"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "earned (production usage across 50+ sessions with EMU corp + personal GitHub accounts)"
|
source: "earned (production usage across 50+ sessions with EMU corp + personal GitHub accounts)"
|
||||||
tools:
|
tools:
|
||||||
- name: "gh"
|
- name: "gh"
|
||||||
description: "GitHub CLI for authenticated operations"
|
description: "GitHub CLI for authenticated operations"
|
||||||
when: "When accessing GitHub resources requiring authentication"
|
when: "When accessing GitHub resources requiring authentication"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Many developers use GitHub through an Enterprise Managed User (EMU) account at work while maintaining a personal GitHub account for open-source contributions. AI agents spawned by Squad inherit the shell's default `gh` authentication — which is usually the EMU account. This causes failures when agents try to push to personal repos, create PRs on forks, or interact with resources outside the enterprise org.
|
Many developers use GitHub through an Enterprise Managed User (EMU) account at work while maintaining a personal GitHub account for open-source contributions. AI agents spawned by Squad inherit the shell's default `gh` authentication — which is usually the EMU account. This causes failures when agents try to push to personal repos, create PRs on forks, or interact with resources outside the enterprise org.
|
||||||
|
|
||||||
This skill teaches agents how to detect the active identity, switch contexts safely, and avoid mixing credentials across operations.
|
This skill teaches agents how to detect the active identity, switch contexts safely, and avoid mixing credentials across operations.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### Detect Current Identity
|
### Detect Current Identity
|
||||||
|
|
||||||
Before any GitHub operation, check which account is active:
|
Before any GitHub operation, check which account is active:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gh auth status
|
gh auth status
|
||||||
```
|
```
|
||||||
|
|
||||||
Look for:
|
Look for:
|
||||||
- `Logged in to github.com as USERNAME` — the active account
|
- `Logged in to github.com as USERNAME` — the active account
|
||||||
- `Token scopes: ...` — what permissions are available
|
- `Token scopes: ...` — what permissions are available
|
||||||
- Multiple accounts will show separate entries
|
- Multiple accounts will show separate entries
|
||||||
|
|
||||||
### Extract a Specific Account's Token
|
### Extract a Specific Account's Token
|
||||||
|
|
||||||
When you need to operate as a specific user (not the default):
|
When you need to operate as a specific user (not the default):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Get the personal account token (by username)
|
# Get the personal account token (by username)
|
||||||
gh auth token --user personaluser
|
gh auth token --user personaluser
|
||||||
|
|
||||||
# Get the EMU account token
|
# Get the EMU account token
|
||||||
gh auth token --user corpalias_enterprise
|
gh auth token --user corpalias_enterprise
|
||||||
```
|
```
|
||||||
|
|
||||||
**Use case:** Push to a personal fork while the default `gh` auth is the EMU account.
|
**Use case:** Push to a personal fork while the default `gh` auth is the EMU account.
|
||||||
|
|
||||||
### Push to Personal Repos from EMU Shell
|
### Push to Personal Repos from EMU Shell
|
||||||
|
|
||||||
The most common scenario: your shell defaults to the EMU account, but you need to push to a personal GitHub repo.
|
The most common scenario: your shell defaults to the EMU account, but you need to push to a personal GitHub repo.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Extract the personal token
|
# 1. Extract the personal token
|
||||||
$token = gh auth token --user personaluser
|
$token = gh auth token --user personaluser
|
||||||
|
|
||||||
# 2. Push using token-authenticated HTTPS
|
# 2. Push using token-authenticated HTTPS
|
||||||
git push https://personaluser:$token@github.com/personaluser/repo.git branch-name
|
git push https://personaluser:$token@github.com/personaluser/repo.git branch-name
|
||||||
```
|
```
|
||||||
|
|
||||||
**Why this works:** `gh auth token --user` reads from `gh`'s credential store without switching the active account. The token is used inline for a single operation and never persisted.
|
**Why this works:** `gh auth token --user` reads from `gh`'s credential store without switching the active account. The token is used inline for a single operation and never persisted.
|
||||||
|
|
||||||
### Create PRs on Personal Forks
|
### Create PRs on Personal Forks
|
||||||
|
|
||||||
When the default `gh` context is EMU but you need to create a PR from a personal fork:
|
When the default `gh` context is EMU but you need to create a PR from a personal fork:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Option 1: Use --repo flag (works if token has access)
|
# Option 1: Use --repo flag (works if token has access)
|
||||||
gh pr create --repo upstream/repo --head personaluser:branch --title "..." --body "..."
|
gh pr create --repo upstream/repo --head personaluser:branch --title "..." --body "..."
|
||||||
|
|
||||||
# Option 2: Temporarily set GH_TOKEN for one command
|
# Option 2: Temporarily set GH_TOKEN for one command
|
||||||
$env:GH_TOKEN = $(gh auth token --user personaluser)
|
$env:GH_TOKEN = $(gh auth token --user personaluser)
|
||||||
gh pr create --repo upstream/repo --head personaluser:branch --title "..."
|
gh pr create --repo upstream/repo --head personaluser:branch --title "..."
|
||||||
Remove-Item Env:\GH_TOKEN
|
Remove-Item Env:\GH_TOKEN
|
||||||
```
|
```
|
||||||
|
|
||||||
### Config Directory Isolation (Advanced)
|
### Config Directory Isolation (Advanced)
|
||||||
|
|
||||||
For complete isolation between accounts, use separate `gh` config directories:
|
For complete isolation between accounts, use separate `gh` config directories:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Personal account operations
|
# Personal account operations
|
||||||
$env:GH_CONFIG_DIR = "$HOME/.config/gh-public"
|
$env:GH_CONFIG_DIR = "$HOME/.config/gh-public"
|
||||||
gh auth login # Login with personal account (one-time setup)
|
gh auth login # Login with personal account (one-time setup)
|
||||||
gh repo clone personaluser/repo
|
gh repo clone personaluser/repo
|
||||||
|
|
||||||
# EMU account operations (default)
|
# EMU account operations (default)
|
||||||
Remove-Item Env:\GH_CONFIG_DIR
|
Remove-Item Env:\GH_CONFIG_DIR
|
||||||
gh auth status # Back to EMU account
|
gh auth status # Back to EMU account
|
||||||
```
|
```
|
||||||
|
|
||||||
**Setup (one-time):**
|
**Setup (one-time):**
|
||||||
```bash
|
```bash
|
||||||
# Create isolated config for personal account
|
# Create isolated config for personal account
|
||||||
mkdir ~/.config/gh-public
|
mkdir ~/.config/gh-public
|
||||||
$env:GH_CONFIG_DIR = "$HOME/.config/gh-public"
|
$env:GH_CONFIG_DIR = "$HOME/.config/gh-public"
|
||||||
gh auth login --web --git-protocol https
|
gh auth login --web --git-protocol https
|
||||||
```
|
```
|
||||||
|
|
||||||
### Shell Aliases for Quick Switching
|
### Shell Aliases for Quick Switching
|
||||||
|
|
||||||
Add to your shell profile for convenience:
|
Add to your shell profile for convenience:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell profile
|
# PowerShell profile
|
||||||
function ghp { $env:GH_CONFIG_DIR = "$HOME/.config/gh-public"; gh @args; Remove-Item Env:\GH_CONFIG_DIR }
|
function ghp { $env:GH_CONFIG_DIR = "$HOME/.config/gh-public"; gh @args; Remove-Item Env:\GH_CONFIG_DIR }
|
||||||
function ghe { gh @args } # Default EMU
|
function ghe { gh @args } # Default EMU
|
||||||
|
|
||||||
# Usage:
|
# Usage:
|
||||||
# ghp repo clone personaluser/repo # Uses personal account
|
# ghp repo clone personaluser/repo # Uses personal account
|
||||||
# ghe issue list # Uses EMU account
|
# ghe issue list # Uses EMU account
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Bash/Zsh profile
|
# Bash/Zsh profile
|
||||||
alias ghp='GH_CONFIG_DIR=~/.config/gh-public gh'
|
alias ghp='GH_CONFIG_DIR=~/.config/gh-public gh'
|
||||||
alias ghe='gh'
|
alias ghe='gh'
|
||||||
|
|
||||||
# Usage:
|
# Usage:
|
||||||
# ghp repo clone personaluser/repo
|
# ghp repo clone personaluser/repo
|
||||||
# ghe issue list
|
# ghe issue list
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### ✓ Correct: Agent pushes blog post to personal GitHub Pages
|
### ✓ Correct: Agent pushes blog post to personal GitHub Pages
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Agent needs to push to personaluser.github.io (personal repo)
|
# Agent needs to push to personaluser.github.io (personal repo)
|
||||||
# Default gh auth is corpalias_enterprise (EMU)
|
# Default gh auth is corpalias_enterprise (EMU)
|
||||||
|
|
||||||
$token = gh auth token --user personaluser
|
$token = gh auth token --user personaluser
|
||||||
git remote set-url origin https://personaluser:$token@github.com/personaluser/personaluser.github.io.git
|
git remote set-url origin https://personaluser:$token@github.com/personaluser/personaluser.github.io.git
|
||||||
git push origin main
|
git push origin main
|
||||||
|
|
||||||
# Clean up — don't leave token in remote URL
|
# Clean up — don't leave token in remote URL
|
||||||
git remote set-url origin https://github.com/personaluser/personaluser.github.io.git
|
git remote set-url origin https://github.com/personaluser/personaluser.github.io.git
|
||||||
```
|
```
|
||||||
|
|
||||||
### ✓ Correct: Agent creates a PR from personal fork to upstream
|
### ✓ Correct: Agent creates a PR from personal fork to upstream
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Fork: personaluser/squad, Upstream: bradygaster/squad
|
# Fork: personaluser/squad, Upstream: bradygaster/squad
|
||||||
# Agent is on branch contrib/fix-docs in the fork clone
|
# Agent is on branch contrib/fix-docs in the fork clone
|
||||||
|
|
||||||
git push origin contrib/fix-docs # Pushes to fork (may need token auth)
|
git push origin contrib/fix-docs # Pushes to fork (may need token auth)
|
||||||
|
|
||||||
# Create PR targeting upstream
|
# Create PR targeting upstream
|
||||||
gh pr create --repo bradygaster/squad --head personaluser:contrib/fix-docs `
|
gh pr create --repo bradygaster/squad --head personaluser:contrib/fix-docs `
|
||||||
--title "docs: fix installation guide" `
|
--title "docs: fix installation guide" `
|
||||||
--body "Fixes #123"
|
--body "Fixes #123"
|
||||||
```
|
```
|
||||||
|
|
||||||
### ✗ Incorrect: Blindly pushing with wrong account
|
### ✗ Incorrect: Blindly pushing with wrong account
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# BAD: Agent assumes default gh auth works for personal repos
|
# BAD: Agent assumes default gh auth works for personal repos
|
||||||
git push origin main
|
git push origin main
|
||||||
# ERROR: Permission denied — EMU account has no access to personal repo
|
# ERROR: Permission denied — EMU account has no access to personal repo
|
||||||
|
|
||||||
# BAD: Hardcoding tokens in scripts
|
# BAD: Hardcoding tokens in scripts
|
||||||
git push https://personaluser:ghp_xxxxxxxxxxxx@github.com/personaluser/repo.git main
|
git push https://personaluser:ghp_xxxxxxxxxxxx@github.com/personaluser/repo.git main
|
||||||
# SECURITY RISK: Token exposed in command history and process list
|
# SECURITY RISK: Token exposed in command history and process list
|
||||||
```
|
```
|
||||||
|
|
||||||
### ✓ Correct: Check before you push
|
### ✓ Correct: Check before you push
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Always verify which account has access before operations
|
# Always verify which account has access before operations
|
||||||
gh auth status
|
gh auth status
|
||||||
# If wrong account, use token extraction:
|
# If wrong account, use token extraction:
|
||||||
$token = gh auth token --user personaluser
|
$token = gh auth token --user personaluser
|
||||||
git push https://personaluser:$token@github.com/personaluser/repo.git main
|
git push https://personaluser:$token@github.com/personaluser/repo.git main
|
||||||
```
|
```
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- ❌ **Hardcoding tokens** in scripts, environment variables, or committed files. Use `gh auth token --user` to extract at runtime.
|
- ❌ **Hardcoding tokens** in scripts, environment variables, or committed files. Use `gh auth token --user` to extract at runtime.
|
||||||
- ❌ **Assuming the default `gh` auth works** for all repos. EMU accounts can't access personal repos and vice versa.
|
- ❌ **Assuming the default `gh` auth works** for all repos. EMU accounts can't access personal repos and vice versa.
|
||||||
- ❌ **Switching `gh auth login`** globally mid-session. This changes the default for ALL processes and can break parallel agents.
|
- ❌ **Switching `gh auth login`** globally mid-session. This changes the default for ALL processes and can break parallel agents.
|
||||||
- ❌ **Storing personal tokens in `.env`** or `.squad/` files. These get committed by Scribe. Use `gh`'s credential store.
|
- ❌ **Storing personal tokens in `.env`** or `.squad/` files. These get committed by Scribe. Use `gh`'s credential store.
|
||||||
- ❌ **Ignoring token cleanup** after inline HTTPS pushes. Always reset the remote URL to avoid persisting tokens.
|
- ❌ **Ignoring token cleanup** after inline HTTPS pushes. Always reset the remote URL to avoid persisting tokens.
|
||||||
- ❌ **Using `gh auth switch`** in multi-agent sessions. One agent switching affects all others sharing the shell.
|
- ❌ **Using `gh auth switch`** in multi-agent sessions. One agent switching affects all others sharing the shell.
|
||||||
- ❌ **Mixing EMU and personal operations** in the same git clone. Use separate clones or explicit remote URLs per operation.
|
- ❌ **Mixing EMU and personal operations** in the same git clone. Use separate clones or explicit remote URLs per operation.
|
||||||
|
|||||||
@@ -1,204 +1,204 @@
|
|||||||
---
|
---
|
||||||
name: "git-workflow"
|
name: "git-workflow"
|
||||||
description: "Squad branching model: dev-first workflow with insiders preview channel"
|
description: "Squad branching model: dev-first workflow with insiders preview channel"
|
||||||
domain: "version-control"
|
domain: "version-control"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "team-decision"
|
source: "team-decision"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Squad uses a three-branch model. **All feature work starts from `dev`, not `main`.**
|
Squad uses a three-branch model. **All feature work starts from `dev`, not `main`.**
|
||||||
|
|
||||||
| Branch | Purpose | Publishes |
|
| Branch | Purpose | Publishes |
|
||||||
|--------|---------|-----------|
|
|--------|---------|-----------|
|
||||||
| `main` | Released, tagged, in-npm code only | `npm publish` on tag |
|
| `main` | Released, tagged, in-npm code only | `npm publish` on tag |
|
||||||
| `dev` | Integration branch — all feature work lands here | `npm publish --tag preview` on merge |
|
| `dev` | Integration branch — all feature work lands here | `npm publish --tag preview` on merge |
|
||||||
| `insiders` | Early-access channel — synced from dev | `npm publish --tag insiders` on sync |
|
| `insiders` | Early-access channel — synced from dev | `npm publish --tag insiders` on sync |
|
||||||
|
|
||||||
## Branch Naming Convention
|
## Branch Naming Convention
|
||||||
|
|
||||||
Issue branches MUST use: `squad/{issue-number}-{kebab-case-slug}`
|
Issue branches MUST use: `squad/{issue-number}-{kebab-case-slug}`
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
- `squad/195-fix-version-stamp-bug`
|
- `squad/195-fix-version-stamp-bug`
|
||||||
- `squad/42-add-profile-api`
|
- `squad/42-add-profile-api`
|
||||||
|
|
||||||
## Workflow for Issue Work
|
## Workflow for Issue Work
|
||||||
|
|
||||||
1. **Branch from dev:**
|
1. **Branch from dev:**
|
||||||
```bash
|
```bash
|
||||||
git checkout dev
|
git checkout dev
|
||||||
git pull origin dev
|
git pull origin dev
|
||||||
git checkout -b squad/{issue-number}-{slug}
|
git checkout -b squad/{issue-number}-{slug}
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Mark issue in-progress:**
|
2. **Mark issue in-progress:**
|
||||||
```bash
|
```bash
|
||||||
gh issue edit {number} --add-label "status:in-progress"
|
gh issue edit {number} --add-label "status:in-progress"
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Create draft PR targeting dev:**
|
3. **Create draft PR targeting dev:**
|
||||||
```bash
|
```bash
|
||||||
gh pr create --base dev --title "{description}" --body "Closes #{issue-number}" --draft
|
gh pr create --base dev --title "{description}" --body "Closes #{issue-number}" --draft
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Do the work.** Make changes, write tests, commit with issue reference.
|
4. **Do the work.** Make changes, write tests, commit with issue reference.
|
||||||
|
|
||||||
5. **Push and mark ready:**
|
5. **Push and mark ready:**
|
||||||
```bash
|
```bash
|
||||||
git push -u origin squad/{issue-number}-{slug}
|
git push -u origin squad/{issue-number}-{slug}
|
||||||
gh pr ready
|
gh pr ready
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **After merge to dev:**
|
6. **After merge to dev:**
|
||||||
```bash
|
```bash
|
||||||
git checkout dev
|
git checkout dev
|
||||||
git pull origin dev
|
git pull origin dev
|
||||||
git branch -d squad/{issue-number}-{slug}
|
git branch -d squad/{issue-number}-{slug}
|
||||||
git push origin --delete squad/{issue-number}-{slug}
|
git push origin --delete squad/{issue-number}-{slug}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Parallel Multi-Issue Work (Worktrees)
|
## Parallel Multi-Issue Work (Worktrees)
|
||||||
|
|
||||||
When the coordinator routes multiple issues simultaneously (e.g., "fix bugs X, Y, and Z"), use `git worktree` to give each agent an isolated working directory. No filesystem collisions, no branch-switching overhead.
|
When the coordinator routes multiple issues simultaneously (e.g., "fix bugs X, Y, and Z"), use `git worktree` to give each agent an isolated working directory. No filesystem collisions, no branch-switching overhead.
|
||||||
|
|
||||||
### When to Use Worktrees vs Sequential
|
### When to Use Worktrees vs Sequential
|
||||||
|
|
||||||
| Scenario | Strategy |
|
| Scenario | Strategy |
|
||||||
|----------|----------|
|
|----------|----------|
|
||||||
| Single issue | Standard workflow above — no worktree needed |
|
| Single issue | Standard workflow above — no worktree needed |
|
||||||
| 2+ simultaneous issues in same repo | Worktrees — one per issue |
|
| 2+ simultaneous issues in same repo | Worktrees — one per issue |
|
||||||
| Work spanning multiple repos | Separate clones as siblings (see Multi-Repo below) |
|
| Work spanning multiple repos | Separate clones as siblings (see Multi-Repo below) |
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
From the main clone (must be on dev or any branch):
|
From the main clone (must be on dev or any branch):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Ensure dev is current
|
# Ensure dev is current
|
||||||
git fetch origin dev
|
git fetch origin dev
|
||||||
|
|
||||||
# Create a worktree per issue — siblings to the main clone
|
# Create a worktree per issue — siblings to the main clone
|
||||||
git worktree add ../squad-195 -b squad/195-fix-stamp-bug origin/dev
|
git worktree add ../squad-195 -b squad/195-fix-stamp-bug origin/dev
|
||||||
git worktree add ../squad-193 -b squad/193-refactor-loader origin/dev
|
git worktree add ../squad-193 -b squad/193-refactor-loader origin/dev
|
||||||
```
|
```
|
||||||
|
|
||||||
**Naming convention:** `../{repo-name}-{issue-number}` (e.g., `../squad-195`, `../squad-pr-42`).
|
**Naming convention:** `../{repo-name}-{issue-number}` (e.g., `../squad-195`, `../squad-pr-42`).
|
||||||
|
|
||||||
Each worktree:
|
Each worktree:
|
||||||
- Has its own working directory and index
|
- Has its own working directory and index
|
||||||
- Is on its own `squad/{issue-number}-{slug}` branch from dev
|
- Is on its own `squad/{issue-number}-{slug}` branch from dev
|
||||||
- Shares the same `.git` object store (disk-efficient)
|
- Shares the same `.git` object store (disk-efficient)
|
||||||
|
|
||||||
### Per-Worktree Agent Workflow
|
### Per-Worktree Agent Workflow
|
||||||
|
|
||||||
Each agent operates inside its worktree exactly like the single-issue workflow:
|
Each agent operates inside its worktree exactly like the single-issue workflow:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ../squad-195
|
cd ../squad-195
|
||||||
|
|
||||||
# Work normally — commits, tests, pushes
|
# Work normally — commits, tests, pushes
|
||||||
git add -A && git commit -m "fix: stamp bug (#195)"
|
git add -A && git commit -m "fix: stamp bug (#195)"
|
||||||
git push -u origin squad/195-fix-stamp-bug
|
git push -u origin squad/195-fix-stamp-bug
|
||||||
|
|
||||||
# Create PR targeting dev
|
# Create PR targeting dev
|
||||||
gh pr create --base dev --title "fix: stamp bug" --body "Closes #195" --draft
|
gh pr create --base dev --title "fix: stamp bug" --body "Closes #195" --draft
|
||||||
```
|
```
|
||||||
|
|
||||||
All PRs target `dev` independently. Agents never interfere with each other's filesystem.
|
All PRs target `dev` independently. Agents never interfere with each other's filesystem.
|
||||||
|
|
||||||
### .squad/ State in Worktrees
|
### .squad/ State in Worktrees
|
||||||
|
|
||||||
The `.squad/` directory exists in each worktree as a copy. This is safe because:
|
The `.squad/` directory exists in each worktree as a copy. This is safe because:
|
||||||
- `.gitattributes` declares `merge=union` on append-only files (history.md, decisions.md, logs)
|
- `.gitattributes` declares `merge=union` on append-only files (history.md, decisions.md, logs)
|
||||||
- Each agent appends to its own section; union merge reconciles on PR merge to dev
|
- Each agent appends to its own section; union merge reconciles on PR merge to dev
|
||||||
- **Rule:** Never rewrite or reorder `.squad/` files in a worktree — append only
|
- **Rule:** Never rewrite or reorder `.squad/` files in a worktree — append only
|
||||||
|
|
||||||
### Cleanup After Merge
|
### Cleanup After Merge
|
||||||
|
|
||||||
After a worktree's PR is merged to dev:
|
After a worktree's PR is merged to dev:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# From the main clone
|
# From the main clone
|
||||||
git worktree remove ../squad-195
|
git worktree remove ../squad-195
|
||||||
git worktree prune # clean stale metadata
|
git worktree prune # clean stale metadata
|
||||||
git branch -d squad/195-fix-stamp-bug
|
git branch -d squad/195-fix-stamp-bug
|
||||||
git push origin --delete squad/195-fix-stamp-bug
|
git push origin --delete squad/195-fix-stamp-bug
|
||||||
```
|
```
|
||||||
|
|
||||||
If a worktree was deleted manually (rm -rf), `git worktree prune` recovers the state.
|
If a worktree was deleted manually (rm -rf), `git worktree prune` recovers the state.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Multi-Repo Downstream Scenarios
|
## Multi-Repo Downstream Scenarios
|
||||||
|
|
||||||
When work spans multiple repositories (e.g., squad-cli changes need squad-sdk changes, or a user's app depends on squad):
|
When work spans multiple repositories (e.g., squad-cli changes need squad-sdk changes, or a user's app depends on squad):
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
Clone downstream repos as siblings to the main repo:
|
Clone downstream repos as siblings to the main repo:
|
||||||
|
|
||||||
```
|
```
|
||||||
~/work/
|
~/work/
|
||||||
squad-pr/ # main repo
|
squad-pr/ # main repo
|
||||||
squad-sdk/ # downstream dependency
|
squad-sdk/ # downstream dependency
|
||||||
user-app/ # consumer project
|
user-app/ # consumer project
|
||||||
```
|
```
|
||||||
|
|
||||||
Each repo gets its own issue branch following its own naming convention. If the downstream repo also uses Squad conventions, use `squad/{issue-number}-{slug}`.
|
Each repo gets its own issue branch following its own naming convention. If the downstream repo also uses Squad conventions, use `squad/{issue-number}-{slug}`.
|
||||||
|
|
||||||
### Coordinated PRs
|
### Coordinated PRs
|
||||||
|
|
||||||
- Create PRs in each repo independently
|
- Create PRs in each repo independently
|
||||||
- Link them in PR descriptions:
|
- Link them in PR descriptions:
|
||||||
```
|
```
|
||||||
Closes #42
|
Closes #42
|
||||||
|
|
||||||
**Depends on:** squad-sdk PR #17 (squad-sdk changes required for this feature)
|
**Depends on:** squad-sdk PR #17 (squad-sdk changes required for this feature)
|
||||||
```
|
```
|
||||||
- Merge order: dependencies first (e.g., squad-sdk), then dependents (e.g., squad-cli)
|
- Merge order: dependencies first (e.g., squad-sdk), then dependents (e.g., squad-cli)
|
||||||
|
|
||||||
### Local Linking for Testing
|
### Local Linking for Testing
|
||||||
|
|
||||||
Before pushing, verify cross-repo changes work together:
|
Before pushing, verify cross-repo changes work together:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Node.js / npm
|
# Node.js / npm
|
||||||
cd ../squad-sdk && npm link
|
cd ../squad-sdk && npm link
|
||||||
cd ../squad-pr && npm link squad-sdk
|
cd ../squad-pr && npm link squad-sdk
|
||||||
|
|
||||||
# Go
|
# Go
|
||||||
# Use replace directive in go.mod:
|
# Use replace directive in go.mod:
|
||||||
# replace github.com/org/squad-sdk => ../squad-sdk
|
# replace github.com/org/squad-sdk => ../squad-sdk
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
cd ../squad-sdk && pip install -e .
|
cd ../squad-sdk && pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
**Important:** Remove local links before committing. `npm link` and `go replace` are dev-only — CI must use published packages or PR-specific refs.
|
**Important:** Remove local links before committing. `npm link` and `go replace` are dev-only — CI must use published packages or PR-specific refs.
|
||||||
|
|
||||||
### Worktrees + Multi-Repo
|
### Worktrees + Multi-Repo
|
||||||
|
|
||||||
These compose naturally. You can have:
|
These compose naturally. You can have:
|
||||||
- Multiple worktrees in the main repo (parallel issues)
|
- Multiple worktrees in the main repo (parallel issues)
|
||||||
- Separate clones for downstream repos
|
- Separate clones for downstream repos
|
||||||
- Each combination operates independently
|
- Each combination operates independently
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- ❌ Branching from main (branch from dev)
|
- ❌ Branching from main (branch from dev)
|
||||||
- ❌ PR targeting main directly (target dev)
|
- ❌ PR targeting main directly (target dev)
|
||||||
- ❌ Non-conforming branch names (must be squad/{number}-{slug})
|
- ❌ Non-conforming branch names (must be squad/{number}-{slug})
|
||||||
- ❌ Committing directly to main or dev (use PRs)
|
- ❌ Committing directly to main or dev (use PRs)
|
||||||
- ❌ Switching branches in the main clone while worktrees are active (use worktrees instead)
|
- ❌ Switching branches in the main clone while worktrees are active (use worktrees instead)
|
||||||
- ❌ Using worktrees for cross-repo work (use separate clones)
|
- ❌ Using worktrees for cross-repo work (use separate clones)
|
||||||
- ❌ Leaving stale worktrees after PR merge (clean up immediately)
|
- ❌ Leaving stale worktrees after PR merge (clean up immediately)
|
||||||
|
|
||||||
## Promotion Pipeline
|
## Promotion Pipeline
|
||||||
|
|
||||||
- dev → insiders: Automated sync on green build
|
- dev → insiders: Automated sync on green build
|
||||||
- dev → main: Manual merge when ready for stable release, then tag
|
- dev → main: Manual merge when ready for stable release, then tag
|
||||||
- Hotfixes: Branch from main as `hotfix/{slug}`, PR to dev, cherry-pick to main if urgent
|
- Hotfixes: Branch from main as `hotfix/{slug}`, PR to dev, cherry-pick to main if urgent
|
||||||
|
|||||||
@@ -1,95 +1,95 @@
|
|||||||
---
|
---
|
||||||
name: github-multi-account
|
name: github-multi-account
|
||||||
description: Detect and set up account-locked gh aliases for multi-account GitHub. The AI reads this skill, detects accounts, asks the user which is personal/work, and runs the setup automatically.
|
description: Detect and set up account-locked gh aliases for multi-account GitHub. The AI reads this skill, detects accounts, asks the user which is personal/work, and runs the setup automatically.
|
||||||
confidence: high
|
confidence: high
|
||||||
source: https://github.com/tamirdresher/squad-skills/tree/main/plugins/github-multi-account
|
source: https://github.com/tamirdresher/squad-skills/tree/main/plugins/github-multi-account
|
||||||
author: tamirdresher
|
author: tamirdresher
|
||||||
---
|
---
|
||||||
|
|
||||||
# GitHub Multi-Account — AI-Driven Setup
|
# GitHub Multi-Account — AI-Driven Setup
|
||||||
|
|
||||||
## When to Activate
|
## When to Activate
|
||||||
When the user has multiple GitHub accounts (check with `gh auth status`). If you see 2+ accounts listed, this skill applies.
|
When the user has multiple GitHub accounts (check with `gh auth status`). If you see 2+ accounts listed, this skill applies.
|
||||||
|
|
||||||
## What to Do (as the AI agent)
|
## What to Do (as the AI agent)
|
||||||
|
|
||||||
### Step 1: Detect accounts
|
### Step 1: Detect accounts
|
||||||
Run: `gh auth status`
|
Run: `gh auth status`
|
||||||
Look for multiple accounts. Note which usernames are listed.
|
Look for multiple accounts. Note which usernames are listed.
|
||||||
|
|
||||||
### Step 2: Ask the user
|
### Step 2: Ask the user
|
||||||
Ask: "I see you have multiple GitHub accounts: {list them}. Which one is your personal account and which is your work/EMU account?"
|
Ask: "I see you have multiple GitHub accounts: {list them}. Which one is your personal account and which is your work/EMU account?"
|
||||||
|
|
||||||
### Step 3: Run the setup automatically
|
### Step 3: Run the setup automatically
|
||||||
Once the user confirms, do ALL of this for them:
|
Once the user confirms, do ALL of this for them:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# 1. Define the functions
|
# 1. Define the functions
|
||||||
$personal = "THEIR_PERSONAL_USERNAME"
|
$personal = "THEIR_PERSONAL_USERNAME"
|
||||||
$work = "THEIR_WORK_USERNAME"
|
$work = "THEIR_WORK_USERNAME"
|
||||||
|
|
||||||
# 2. Add to PowerShell profile
|
# 2. Add to PowerShell profile
|
||||||
$profilePath = $PROFILE.CurrentUserAllHosts
|
$profilePath = $PROFILE.CurrentUserAllHosts
|
||||||
if (!(Test-Path $profilePath)) { New-Item -Path $profilePath -Force | Out-Null }
|
if (!(Test-Path $profilePath)) { New-Item -Path $profilePath -Force | Out-Null }
|
||||||
$existing = Get-Content $profilePath -Raw -ErrorAction SilentlyContinue
|
$existing = Get-Content $profilePath -Raw -ErrorAction SilentlyContinue
|
||||||
if ($existing -notmatch "gh-personal") {
|
if ($existing -notmatch "gh-personal") {
|
||||||
$block = @"
|
$block = @"
|
||||||
|
|
||||||
# === GitHub Multi-Account Aliases ===
|
# === GitHub Multi-Account Aliases ===
|
||||||
function gh-personal { gh auth switch --user $personal 2>`$null | Out-Null; gh @args }
|
function gh-personal { gh auth switch --user $personal 2>`$null | Out-Null; gh @args }
|
||||||
function gh-work { gh auth switch --user $work 2>`$null | Out-Null; gh @args }
|
function gh-work { gh auth switch --user $work 2>`$null | Out-Null; gh @args }
|
||||||
Set-Alias ghp gh-personal
|
Set-Alias ghp gh-personal
|
||||||
Set-Alias ghw gh-work
|
Set-Alias ghw gh-work
|
||||||
"@
|
"@
|
||||||
Add-Content -Path $profilePath -Value $block
|
Add-Content -Path $profilePath -Value $block
|
||||||
}
|
}
|
||||||
|
|
||||||
# 3. Create CMD wrappers
|
# 3. Create CMD wrappers
|
||||||
$binDir = Join-Path $env:USERPROFILE ".squad\bin"
|
$binDir = Join-Path $env:USERPROFILE ".squad\bin"
|
||||||
if (!(Test-Path $binDir)) { New-Item -ItemType Directory -Path $binDir -Force | Out-Null }
|
if (!(Test-Path $binDir)) { New-Item -ItemType Directory -Path $binDir -Force | Out-Null }
|
||||||
"@echo off`ngh auth switch --user $personal >nul 2>&1`ngh %*" | Out-File "$binDir\ghp.cmd" -Encoding ascii
|
"@echo off`ngh auth switch --user $personal >nul 2>&1`ngh %*" | Out-File "$binDir\ghp.cmd" -Encoding ascii
|
||||||
"@echo off`ngh auth switch --user $work >nul 2>&1`ngh %*" | Out-File "$binDir\ghw.cmd" -Encoding ascii
|
"@echo off`ngh auth switch --user $work >nul 2>&1`ngh %*" | Out-File "$binDir\ghw.cmd" -Encoding ascii
|
||||||
|
|
||||||
# 4. Add to PATH
|
# 4. Add to PATH
|
||||||
$userPath = [Environment]::GetEnvironmentVariable("PATH", "User")
|
$userPath = [Environment]::GetEnvironmentVariable("PATH", "User")
|
||||||
if ($userPath -notmatch [regex]::Escape($binDir)) {
|
if ($userPath -notmatch [regex]::Escape($binDir)) {
|
||||||
[Environment]::SetEnvironmentVariable("PATH", "$binDir;$userPath", "User")
|
[Environment]::SetEnvironmentVariable("PATH", "$binDir;$userPath", "User")
|
||||||
$env:PATH = "$binDir;$env:PATH"
|
$env:PATH = "$binDir;$env:PATH"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 5. Load in current session
|
# 5. Load in current session
|
||||||
function gh-personal { gh auth switch --user $personal 2>$null | Out-Null; gh @args }
|
function gh-personal { gh auth switch --user $personal 2>$null | Out-Null; gh @args }
|
||||||
function gh-work { gh auth switch --user $work 2>$null | Out-Null; gh @args }
|
function gh-work { gh auth switch --user $work 2>$null | Out-Null; gh @args }
|
||||||
Set-Alias ghp gh-personal
|
Set-Alias ghp gh-personal
|
||||||
Set-Alias ghw gh-work
|
Set-Alias ghw gh-work
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 4: Verify for the user
|
### Step 4: Verify for the user
|
||||||
Run these and show the results:
|
Run these and show the results:
|
||||||
```powershell
|
```powershell
|
||||||
ghp api user --jq '.login' # should show personal username
|
ghp api user --jq '.login' # should show personal username
|
||||||
ghw api user --jq '.login' # should show work username
|
ghw api user --jq '.login' # should show work username
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 5: Tell the user
|
### Step 5: Tell the user
|
||||||
"All set! From now on use `ghp` for personal repos and `ghw` for work repos. I'll use them too."
|
"All set! From now on use `ghp` for personal repos and `ghw` for work repos. I'll use them too."
|
||||||
|
|
||||||
## After Setup — Usage Rules
|
## After Setup — Usage Rules
|
||||||
|
|
||||||
1. **NEVER** use bare `gh` for repo operations — always `ghp` or `ghw`
|
1. **NEVER** use bare `gh` for repo operations — always `ghp` or `ghw`
|
||||||
2. **NEVER** manually `gh auth switch` — the aliases handle it
|
2. **NEVER** manually `gh auth switch` — the aliases handle it
|
||||||
3. Determine alias by repo owner:
|
3. Determine alias by repo owner:
|
||||||
- Personal account repos → `ghp` / `gh-personal`
|
- Personal account repos → `ghp` / `gh-personal`
|
||||||
- Work/EMU account repos → `ghw` / `gh-work`
|
- Work/EMU account repos → `ghw` / `gh-work`
|
||||||
|
|
||||||
## Repo-Specific Account Binding
|
## Repo-Specific Account Binding
|
||||||
|
|
||||||
This repo (`bradygaster/squad`) is bound to the **bradygaster** (personal) account.
|
This repo (`bradygaster/squad`) is bound to the **bradygaster** (personal) account.
|
||||||
All `gh` operations in this repo MUST use `ghp` / `gh-personal`.
|
All `gh` operations in this repo MUST use `ghp` / `gh-personal`.
|
||||||
|
|
||||||
## For Squad Agents
|
## For Squad Agents
|
||||||
At the TOP of any script touching GitHub, define:
|
At the TOP of any script touching GitHub, define:
|
||||||
```powershell
|
```powershell
|
||||||
function gh-personal { gh auth switch --user bradygaster 2>$null | Out-Null; gh @args }
|
function gh-personal { gh auth switch --user bradygaster 2>$null | Out-Null; gh @args }
|
||||||
function gh-work { gh auth switch --user bradyg_microsoft 2>$null | Out-Null; gh @args }
|
function gh-work { gh auth switch --user bradyg_microsoft 2>$null | Out-Null; gh @args }
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
---
|
---
|
||||||
name: history-hygiene
|
name: history-hygiene
|
||||||
description: Record final outcomes to history.md, not intermediate requests or reversed decisions
|
description: Record final outcomes to history.md, not intermediate requests or reversed decisions
|
||||||
domain: documentation, team-collaboration
|
domain: documentation, team-collaboration
|
||||||
confidence: high
|
confidence: high
|
||||||
source: earned (Kobayashi v0.6.0 incident, team intervention)
|
source: earned (Kobayashi v0.6.0 incident, team intervention)
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
History files (.md files tracking decisions, spawns, outcomes) are read cold by future agents. Stale or incorrect entries poison decision-making downstream. The Kobayashi incident proved this: history said "Brady decided v0.6.0" when Brady had reversed that to v0.8.17. Future spawns read the wrong truth and repeated the mistake.
|
History files (.md files tracking decisions, spawns, outcomes) are read cold by future agents. Stale or incorrect entries poison decision-making downstream. The Kobayashi incident proved this: history said "Brady decided v0.6.0" when Brady had reversed that to v0.8.17. Future spawns read the wrong truth and repeated the mistake.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
- **Record the final outcome**, not the initial request.
|
- **Record the final outcome**, not the initial request.
|
||||||
- **Wait for confirmation** before writing to history — don't log intermediate states.
|
- **Wait for confirmation** before writing to history — don't log intermediate states.
|
||||||
- **If a decision reverses**, update the entry immediately — don't leave stale data.
|
- **If a decision reverses**, update the entry immediately — don't leave stale data.
|
||||||
- **One read = one truth.** A future agent should never need to cross-reference other files to understand what actually happened.
|
- **One read = one truth.** A future agent should never need to cross-reference other files to understand what actually happened.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
✓ **Correct:**
|
✓ **Correct:**
|
||||||
- "Migration target: v0.8.17 (initially discussed as v0.6.0, corrected by Brady)"
|
- "Migration target: v0.8.17 (initially discussed as v0.6.0, corrected by Brady)"
|
||||||
- "Reverted to Node 18 per Brady's explicit request on 2024-01-15"
|
- "Reverted to Node 18 per Brady's explicit request on 2024-01-15"
|
||||||
|
|
||||||
✗ **Incorrect:**
|
✗ **Incorrect:**
|
||||||
- "Brady directed v0.6.0" (when later reversed)
|
- "Brady directed v0.6.0" (when later reversed)
|
||||||
- Recording what was *requested* instead of what *actually happened*
|
- Recording what was *requested* instead of what *actually happened*
|
||||||
- Logging entries before outcome is confirmed
|
- Logging entries before outcome is confirmed
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- Writing intermediate or "for now" states to disk
|
- Writing intermediate or "for now" states to disk
|
||||||
- Attributing decisions without confirming final direction
|
- Attributing decisions without confirming final direction
|
||||||
- Treating history like a draft — history is the source of truth
|
- Treating history like a draft — history is the source of truth
|
||||||
- Assuming readers will cross-reference or verify; they won't
|
- Assuming readers will cross-reference or verify; they won't
|
||||||
|
|||||||
@@ -1,105 +1,105 @@
|
|||||||
---
|
---
|
||||||
name: "humanizer"
|
name: "humanizer"
|
||||||
description: "Tone enforcement patterns for external-facing community responses"
|
description: "Tone enforcement patterns for external-facing community responses"
|
||||||
domain: "communication, tone, community"
|
domain: "communication, tone, community"
|
||||||
confidence: "low"
|
confidence: "low"
|
||||||
source: "manual (RFC #426 — PAO External Communications)"
|
source: "manual (RFC #426 — PAO External Communications)"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Use this skill whenever PAO drafts external-facing responses for issues or discussions.
|
Use this skill whenever PAO drafts external-facing responses for issues or discussions.
|
||||||
|
|
||||||
- Tone must be warm, helpful, and human-sounding — never robotic or corporate.
|
- Tone must be warm, helpful, and human-sounding — never robotic or corporate.
|
||||||
- Brady's constraint applies everywhere: **Humanized tone is mandatory**.
|
- Brady's constraint applies everywhere: **Humanized tone is mandatory**.
|
||||||
- This applies to **all external-facing content** drafted by PAO in Phase 1 issues/discussions workflows.
|
- This applies to **all external-facing content** drafted by PAO in Phase 1 issues/discussions workflows.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
1. **Warm opening** — Start with acknowledgment ("Thanks for reporting this", "Great question!")
|
1. **Warm opening** — Start with acknowledgment ("Thanks for reporting this", "Great question!")
|
||||||
2. **Active voice** — "We're looking into this" not "This is being investigated"
|
2. **Active voice** — "We're looking into this" not "This is being investigated"
|
||||||
3. **Second person** — Address the person directly ("you" not "the user")
|
3. **Second person** — Address the person directly ("you" not "the user")
|
||||||
4. **Conversational connectors** — "That said...", "Here's what we found...", "Quick note:"
|
4. **Conversational connectors** — "That said...", "Here's what we found...", "Quick note:"
|
||||||
5. **Specific, not vague** — "This affects the casting module in v0.8.x" not "We are aware of issues"
|
5. **Specific, not vague** — "This affects the casting module in v0.8.x" not "We are aware of issues"
|
||||||
6. **Empathy markers** — "I can see how that would be frustrating", "Good catch!"
|
6. **Empathy markers** — "I can see how that would be frustrating", "Good catch!"
|
||||||
7. **Action-oriented closes** — "Let us know if that helps!" not "Please advise if further assistance is required"
|
7. **Action-oriented closes** — "Let us know if that helps!" not "Please advise if further assistance is required"
|
||||||
8. **Uncertainty is OK** — "We're not 100% sure yet, but here's what we think is happening..." is better than false confidence
|
8. **Uncertainty is OK** — "We're not 100% sure yet, but here's what we think is happening..." is better than false confidence
|
||||||
9. **Profanity filter** — Never include profanity, slurs, or aggressive language, even when quoting
|
9. **Profanity filter** — Never include profanity, slurs, or aggressive language, even when quoting
|
||||||
10. **Baseline comparison** — Responses should align with tone of 5-10 "gold standard" responses (>80% similarity threshold)
|
10. **Baseline comparison** — Responses should align with tone of 5-10 "gold standard" responses (>80% similarity threshold)
|
||||||
11. **Empathetic disagreement** — "We hear you. That's a fair concern." before explaining the reasoning
|
11. **Empathetic disagreement** — "We hear you. That's a fair concern." before explaining the reasoning
|
||||||
12. **Information request** — Ask for specific details, not open-ended "can you provide more info?"
|
12. **Information request** — Ask for specific details, not open-ended "can you provide more info?"
|
||||||
13. **No link-dumping** — Don't just paste URLs. Provide context: "Check out the [getting started guide](url) — specifically the section on routing" not just a bare link
|
13. **No link-dumping** — Don't just paste URLs. Provide context: "Check out the [getting started guide](url) — specifically the section on routing" not just a bare link
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### 1. Welcome
|
### 1. Welcome
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Hey {author}! Welcome to Squad 👋 Thanks for opening this.
|
Hey {author}! Welcome to Squad 👋 Thanks for opening this.
|
||||||
{substantive response}
|
{substantive response}
|
||||||
Let us know if you have questions — happy to help!
|
Let us know if you have questions — happy to help!
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Troubleshooting
|
### 2. Troubleshooting
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Thanks for the detailed report, {author}!
|
Thanks for the detailed report, {author}!
|
||||||
Here's what we think is happening: {explanation}
|
Here's what we think is happening: {explanation}
|
||||||
{steps or workaround}
|
{steps or workaround}
|
||||||
Let us know if that helps, or if you're seeing something different.
|
Let us know if that helps, or if you're seeing something different.
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Feature guidance
|
### 3. Feature guidance
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Great question! {context on current state}
|
Great question! {context on current state}
|
||||||
{guidance or workaround}
|
{guidance or workaround}
|
||||||
We've noted this as a potential improvement — {tracking info if applicable}.
|
We've noted this as a potential improvement — {tracking info if applicable}.
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Redirect
|
### 4. Redirect
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Thanks for reaching out! This one is actually better suited for {correct location}.
|
Thanks for reaching out! This one is actually better suited for {correct location}.
|
||||||
{brief explanation of why}
|
{brief explanation of why}
|
||||||
Feel free to open it there — they'll be able to help!
|
Feel free to open it there — they'll be able to help!
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. Acknowledgment
|
### 5. Acknowledgment
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Good catch, {author}. We've confirmed this is a real issue.
|
Good catch, {author}. We've confirmed this is a real issue.
|
||||||
{what we know so far}
|
{what we know so far}
|
||||||
We'll update this thread when we have a fix. Thanks for flagging it!
|
We'll update this thread when we have a fix. Thanks for flagging it!
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. Closing
|
### 6. Closing
|
||||||
|
|
||||||
```text
|
```text
|
||||||
This should be resolved in {version/PR}! 🎉
|
This should be resolved in {version/PR}! 🎉
|
||||||
{brief summary of what changed}
|
{brief summary of what changed}
|
||||||
Thanks for reporting this, {author} — it made Squad better.
|
Thanks for reporting this, {author} — it made Squad better.
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. Technical uncertainty
|
### 7. Technical uncertainty
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Interesting find, {author}. We're not 100% sure what's causing this yet.
|
Interesting find, {author}. We're not 100% sure what's causing this yet.
|
||||||
Here's what we've ruled out: {list}
|
Here's what we've ruled out: {list}
|
||||||
We'd love more context if you have it — {specific ask}.
|
We'd love more context if you have it — {specific ask}.
|
||||||
We'll dig deeper and update this thread.
|
We'll dig deeper and update this thread.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- ❌ Corporate speak: "We appreciate your patience as we investigate this matter"
|
- ❌ Corporate speak: "We appreciate your patience as we investigate this matter"
|
||||||
- ❌ Marketing hype: "Squad is the BEST way to..." or "This amazing feature..."
|
- ❌ Marketing hype: "Squad is the BEST way to..." or "This amazing feature..."
|
||||||
- ❌ Passive voice: "It has been determined that..." or "The issue is being tracked"
|
- ❌ Passive voice: "It has been determined that..." or "The issue is being tracked"
|
||||||
- ❌ Dismissive: "This works as designed" without empathy
|
- ❌ Dismissive: "This works as designed" without empathy
|
||||||
- ❌ Over-promising: "We'll ship this next week" without commitment from the team
|
- ❌ Over-promising: "We'll ship this next week" without commitment from the team
|
||||||
- ❌ Empty acknowledgment: "Thanks for your feedback" with no substance
|
- ❌ Empty acknowledgment: "Thanks for your feedback" with no substance
|
||||||
- ❌ Robot signatures: "Best regards, PAO" or "Sincerely, The Squad Team"
|
- ❌ Robot signatures: "Best regards, PAO" or "Sincerely, The Squad Team"
|
||||||
- ❌ Excessive emoji: More than 1-2 emoji per response
|
- ❌ Excessive emoji: More than 1-2 emoji per response
|
||||||
- ❌ Quoting profanity: Even when the original issue contains it, paraphrase instead
|
- ❌ Quoting profanity: Even when the original issue contains it, paraphrase instead
|
||||||
- ❌ Link-dumping: Pasting URLs without context ("See: https://...")
|
- ❌ Link-dumping: Pasting URLs without context ("See: https://...")
|
||||||
- ❌ Open-ended info requests: "Can you provide more information?" without specifying what information
|
- ❌ Open-ended info requests: "Can you provide more information?" without specifying what information
|
||||||
|
|||||||
@@ -1,102 +1,102 @@
|
|||||||
---
|
---
|
||||||
name: "init-mode"
|
name: "init-mode"
|
||||||
description: "Team initialization flow (Phase 1 proposal + Phase 2 creation)"
|
description: "Team initialization flow (Phase 1 proposal + Phase 2 creation)"
|
||||||
domain: "orchestration"
|
domain: "orchestration"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "extracted"
|
source: "extracted"
|
||||||
tools:
|
tools:
|
||||||
- name: "ask_user"
|
- name: "ask_user"
|
||||||
description: "Confirm team roster with selectable menu"
|
description: "Confirm team roster with selectable menu"
|
||||||
when: "Phase 1 proposal — requires explicit user confirmation"
|
when: "Phase 1 proposal — requires explicit user confirmation"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Init Mode activates when `.squad/team.md` does not exist, or exists but has zero roster entries under `## Members`. The coordinator proposes a team (Phase 1), waits for user confirmation, then creates the team structure (Phase 2).
|
Init Mode activates when `.squad/team.md` does not exist, or exists but has zero roster entries under `## Members`. The coordinator proposes a team (Phase 1), waits for user confirmation, then creates the team structure (Phase 2).
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### Phase 1: Propose the Team
|
### Phase 1: Propose the Team
|
||||||
|
|
||||||
No team exists yet. Propose one — but **DO NOT create any files until the user confirms.**
|
No team exists yet. Propose one — but **DO NOT create any files until the user confirms.**
|
||||||
|
|
||||||
1. **Identify the user.** Run `git config user.name` to learn who you're working with. Use their name in conversation (e.g., *"Hey Brady, what are you building?"*). Store their name (NOT email) in `team.md` under Project Context. **Never read or store `git config user.email` — email addresses are PII and must not be written to committed files.**
|
1. **Identify the user.** Run `git config user.name` to learn who you're working with. Use their name in conversation (e.g., *"Hey Brady, what are you building?"*). Store their name (NOT email) in `team.md` under Project Context. **Never read or store `git config user.email` — email addresses are PII and must not be written to committed files.**
|
||||||
2. Ask: *"What are you building? (language, stack, what it does)"*
|
2. Ask: *"What are you building? (language, stack, what it does)"*
|
||||||
3. **Cast the team.** Before proposing names, run the Casting & Persistent Naming algorithm (see that section):
|
3. **Cast the team.** Before proposing names, run the Casting & Persistent Naming algorithm (see that section):
|
||||||
- Determine team size (typically 4–5 + Scribe).
|
- Determine team size (typically 4–5 + Scribe).
|
||||||
- Determine assignment shape from the user's project description.
|
- Determine assignment shape from the user's project description.
|
||||||
- Derive resonance signals from the session and repo context.
|
- Derive resonance signals from the session and repo context.
|
||||||
- Select a universe. If the universe is custom, allocate character names from that universe based on the related list found in the `.squad/templates/casting/` directory. Prefer custom universes when available.
|
- Select a universe. If the universe is custom, allocate character names from that universe based on the related list found in the `.squad/templates/casting/` directory. Prefer custom universes when available.
|
||||||
- Scribe is always "Scribe" — exempt from casting.
|
- Scribe is always "Scribe" — exempt from casting.
|
||||||
- Ralph is always "Ralph" — exempt from casting.
|
- Ralph is always "Ralph" — exempt from casting.
|
||||||
4. Propose the team with their cast names. Example (names will vary per cast):
|
4. Propose the team with their cast names. Example (names will vary per cast):
|
||||||
|
|
||||||
```
|
```
|
||||||
🏗️ {CastName1} — Lead Scope, decisions, code review
|
🏗️ {CastName1} — Lead Scope, decisions, code review
|
||||||
⚛️ {CastName2} — Frontend Dev React, UI, components
|
⚛️ {CastName2} — Frontend Dev React, UI, components
|
||||||
🔧 {CastName3} — Backend Dev APIs, database, services
|
🔧 {CastName3} — Backend Dev APIs, database, services
|
||||||
🧪 {CastName4} — Tester Tests, quality, edge cases
|
🧪 {CastName4} — Tester Tests, quality, edge cases
|
||||||
📋 Scribe — (silent) Memory, decisions, session logs
|
📋 Scribe — (silent) Memory, decisions, session logs
|
||||||
🔄 Ralph — (monitor) Work queue, backlog, keep-alive
|
🔄 Ralph — (monitor) Work queue, backlog, keep-alive
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Use the `ask_user` tool to confirm the roster. Provide choices so the user sees a selectable menu:
|
5. Use the `ask_user` tool to confirm the roster. Provide choices so the user sees a selectable menu:
|
||||||
- **question:** *"Look right?"*
|
- **question:** *"Look right?"*
|
||||||
- **choices:** `["Yes, hire this team", "Add someone", "Change a role"]`
|
- **choices:** `["Yes, hire this team", "Add someone", "Change a role"]`
|
||||||
|
|
||||||
**⚠️ STOP. Your response ENDS here. Do NOT proceed to Phase 2. Do NOT create any files or directories. Wait for the user's reply.**
|
**⚠️ STOP. Your response ENDS here. Do NOT proceed to Phase 2. Do NOT create any files or directories. Wait for the user's reply.**
|
||||||
|
|
||||||
### Phase 2: Create the Team
|
### Phase 2: Create the Team
|
||||||
|
|
||||||
**Trigger:** The user replied to Phase 1 with confirmation ("yes", "looks good", or similar affirmative), OR the user's reply to Phase 1 is a task (treat as implicit "yes").
|
**Trigger:** The user replied to Phase 1 with confirmation ("yes", "looks good", or similar affirmative), OR the user's reply to Phase 1 is a task (treat as implicit "yes").
|
||||||
|
|
||||||
> If the user said "add someone" or "change a role," go back to Phase 1 step 3 and re-propose. Do NOT enter Phase 2 until the user confirms.
|
> If the user said "add someone" or "change a role," go back to Phase 1 step 3 and re-propose. Do NOT enter Phase 2 until the user confirms.
|
||||||
|
|
||||||
6. Create the `.squad/` directory structure (see `.squad/templates/` for format guides or use the standard structure: team.md, routing.md, ceremonies.md, decisions.md, decisions/inbox/, casting/, agents/, orchestration-log/, skills/, log/).
|
6. Create the `.squad/` directory structure (see `.squad/templates/` for format guides or use the standard structure: team.md, routing.md, ceremonies.md, decisions.md, decisions/inbox/, casting/, agents/, orchestration-log/, skills/, log/).
|
||||||
|
|
||||||
**Casting state initialization:** Copy `.squad/templates/casting-policy.json` to `.squad/casting/policy.json` (or create from defaults). Create `registry.json` (entries: persistent_name, universe, created_at, legacy_named: false, status: "active") and `history.json` (first assignment snapshot with unique assignment_id).
|
**Casting state initialization:** Copy `.squad/templates/casting-policy.json` to `.squad/casting/policy.json` (or create from defaults). Create `registry.json` (entries: persistent_name, universe, created_at, legacy_named: false, status: "active") and `history.json` (first assignment snapshot with unique assignment_id).
|
||||||
|
|
||||||
**Seeding:** Each agent's `history.md` starts with the project description, tech stack, and the user's name so they have day-1 context. Agent folder names are the cast name in lowercase (e.g., `.squad/agents/ripley/`). The Scribe's charter includes maintaining `decisions.md` and cross-agent context sharing.
|
**Seeding:** Each agent's `history.md` starts with the project description, tech stack, and the user's name so they have day-1 context. Agent folder names are the cast name in lowercase (e.g., `.squad/agents/ripley/`). The Scribe's charter includes maintaining `decisions.md` and cross-agent context sharing.
|
||||||
|
|
||||||
**Team.md structure:** `team.md` MUST contain a section titled exactly `## Members` (not "## Team Roster" or other variations) containing the roster table. This header is hard-coded in GitHub workflows (`squad-heartbeat.yml`, `squad-issue-assign.yml`, `squad-triage.yml`, `sync-squad-labels.yml`) for label automation. If the header is missing or titled differently, label routing breaks.
|
**Team.md structure:** `team.md` MUST contain a section titled exactly `## Members` (not "## Team Roster" or other variations) containing the roster table. This header is hard-coded in GitHub workflows (`squad-heartbeat.yml`, `squad-issue-assign.yml`, `squad-triage.yml`, `sync-squad-labels.yml`) for label automation. If the header is missing or titled differently, label routing breaks.
|
||||||
|
|
||||||
**Merge driver for append-only files:** Create or update `.gitattributes` at the repo root to enable conflict-free merging of `.squad/` state across branches:
|
**Merge driver for append-only files:** Create or update `.gitattributes` at the repo root to enable conflict-free merging of `.squad/` state across branches:
|
||||||
```
|
```
|
||||||
.squad/decisions.md merge=union
|
.squad/decisions.md merge=union
|
||||||
.squad/agents/*/history.md merge=union
|
.squad/agents/*/history.md merge=union
|
||||||
.squad/log/** merge=union
|
.squad/log/** merge=union
|
||||||
.squad/orchestration-log/** merge=union
|
.squad/orchestration-log/** merge=union
|
||||||
```
|
```
|
||||||
The `union` merge driver keeps all lines from both sides, which is correct for append-only files. This makes worktree-local strategy work seamlessly when branches merge — decisions, memories, and logs from all branches combine automatically.
|
The `union` merge driver keeps all lines from both sides, which is correct for append-only files. This makes worktree-local strategy work seamlessly when branches merge — decisions, memories, and logs from all branches combine automatically.
|
||||||
|
|
||||||
7. Say: *"✅ Team hired. Try: '{FirstCastName}, set up the project structure'"*
|
7. Say: *"✅ Team hired. Try: '{FirstCastName}, set up the project structure'"*
|
||||||
|
|
||||||
8. **Post-setup input sources** (optional — ask after team is created, not during casting):
|
8. **Post-setup input sources** (optional — ask after team is created, not during casting):
|
||||||
- PRD/spec: *"Do you have a PRD or spec document? (file path, paste it, or skip)"* → If provided, follow PRD Mode flow
|
- PRD/spec: *"Do you have a PRD or spec document? (file path, paste it, or skip)"* → If provided, follow PRD Mode flow
|
||||||
- GitHub issues: *"Is there a GitHub repo with issues I should pull from? (owner/repo, or skip)"* → If provided, follow GitHub Issues Mode flow
|
- GitHub issues: *"Is there a GitHub repo with issues I should pull from? (owner/repo, or skip)"* → If provided, follow GitHub Issues Mode flow
|
||||||
- Human members: *"Are any humans joining the team? (names and roles, or just AI for now)"* → If provided, add per Human Team Members section
|
- Human members: *"Are any humans joining the team? (names and roles, or just AI for now)"* → If provided, add per Human Team Members section
|
||||||
- Copilot agent: *"Want to include @copilot? It can pick up issues autonomously. (yes/no)"* → If yes, follow Copilot Coding Agent Member section and ask about auto-assignment
|
- Copilot agent: *"Want to include @copilot? It can pick up issues autonomously. (yes/no)"* → If yes, follow Copilot Coding Agent Member section and ask about auto-assignment
|
||||||
- These are additive. Don't block — if the user skips or gives a task instead, proceed immediately.
|
- These are additive. Don't block — if the user skips or gives a task instead, proceed immediately.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
**Example flow:**
|
**Example flow:**
|
||||||
1. Coordinator detects no team.md → Init Mode
|
1. Coordinator detects no team.md → Init Mode
|
||||||
2. Runs `git config user.name` → "Brady"
|
2. Runs `git config user.name` → "Brady"
|
||||||
3. Asks: *"Hey Brady, what are you building?"*
|
3. Asks: *"Hey Brady, what are you building?"*
|
||||||
4. User: *"TypeScript CLI tool with GitHub API integration"*
|
4. User: *"TypeScript CLI tool with GitHub API integration"*
|
||||||
5. Coordinator runs casting algorithm → selects "The Usual Suspects" universe
|
5. Coordinator runs casting algorithm → selects "The Usual Suspects" universe
|
||||||
6. Proposes: Keaton (Lead), Verbal (Prompt), Fenster (Backend), Hockney (Tester), Scribe, Ralph
|
6. Proposes: Keaton (Lead), Verbal (Prompt), Fenster (Backend), Hockney (Tester), Scribe, Ralph
|
||||||
7. Uses `ask_user` with choices → user selects "Yes, hire this team"
|
7. Uses `ask_user` with choices → user selects "Yes, hire this team"
|
||||||
8. Coordinator creates `.squad/` structure, initializes casting state, seeds agents
|
8. Coordinator creates `.squad/` structure, initializes casting state, seeds agents
|
||||||
9. Says: *"✅ Team hired. Try: 'Keaton, set up the project structure'"*
|
9. Says: *"✅ Team hired. Try: 'Keaton, set up the project structure'"*
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- ❌ Creating files before user confirms Phase 1
|
- ❌ Creating files before user confirms Phase 1
|
||||||
- ❌ Mixing agents from different universes in the same cast
|
- ❌ Mixing agents from different universes in the same cast
|
||||||
- ❌ Skipping the `ask_user` tool and assuming confirmation
|
- ❌ Skipping the `ask_user` tool and assuming confirmation
|
||||||
- ❌ Proceeding to Phase 2 when user said "add someone" or "change a role"
|
- ❌ Proceeding to Phase 2 when user said "add someone" or "change a role"
|
||||||
- ❌ Using `## Team Roster` instead of `## Members` as the header (breaks GitHub workflows)
|
- ❌ Using `## Team Roster` instead of `## Members` as the header (breaks GitHub workflows)
|
||||||
- ❌ Forgetting to initialize `.squad/casting/` state files
|
- ❌ Forgetting to initialize `.squad/casting/` state files
|
||||||
- ❌ Reading or storing `git config user.email` (PII violation)
|
- ❌ Reading or storing `git config user.email` (PII violation)
|
||||||
|
|||||||
@@ -1,117 +1,117 @@
|
|||||||
# Model Selection
|
# Model Selection
|
||||||
|
|
||||||
> Determines which LLM model to use for each agent spawn.
|
> Determines which LLM model to use for each agent spawn.
|
||||||
|
|
||||||
## SCOPE
|
## SCOPE
|
||||||
|
|
||||||
✅ THIS SKILL PRODUCES:
|
✅ THIS SKILL PRODUCES:
|
||||||
- A resolved `model` parameter for every `task` tool call
|
- A resolved `model` parameter for every `task` tool call
|
||||||
- Persistent model preferences in `.squad/config.json`
|
- Persistent model preferences in `.squad/config.json`
|
||||||
- Spawn acknowledgments that include the resolved model
|
- Spawn acknowledgments that include the resolved model
|
||||||
|
|
||||||
❌ THIS SKILL DOES NOT PRODUCE:
|
❌ THIS SKILL DOES NOT PRODUCE:
|
||||||
- Code, tests, or documentation
|
- Code, tests, or documentation
|
||||||
- Model performance benchmarks
|
- Model performance benchmarks
|
||||||
- Cost reports or billing artifacts
|
- Cost reports or billing artifacts
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Squad supports 18+ models across three tiers (premium, standard, fast). The coordinator must select the right model for each agent spawn. Users can set persistent preferences that survive across sessions.
|
Squad supports 18+ models across three tiers (premium, standard, fast). The coordinator must select the right model for each agent spawn. Users can set persistent preferences that survive across sessions.
|
||||||
|
|
||||||
## 5-Layer Model Resolution Hierarchy
|
## 5-Layer Model Resolution Hierarchy
|
||||||
|
|
||||||
Resolution is **first-match-wins** — the highest layer with a value wins.
|
Resolution is **first-match-wins** — the highest layer with a value wins.
|
||||||
|
|
||||||
| Layer | Name | Source | Persistence |
|
| Layer | Name | Source | Persistence |
|
||||||
|-------|------|--------|-------------|
|
|-------|------|--------|-------------|
|
||||||
| **0a** | Per-Agent Config | `.squad/config.json` → `agentModelOverrides.{name}` | Persistent (survives sessions) |
|
| **0a** | Per-Agent Config | `.squad/config.json` → `agentModelOverrides.{name}` | Persistent (survives sessions) |
|
||||||
| **0b** | Global Config | `.squad/config.json` → `defaultModel` | Persistent (survives sessions) |
|
| **0b** | Global Config | `.squad/config.json` → `defaultModel` | Persistent (survives sessions) |
|
||||||
| **1** | Session Directive | User said "use X" in current session | Session-only |
|
| **1** | Session Directive | User said "use X" in current session | Session-only |
|
||||||
| **2** | Charter Preference | Agent's `charter.md` → `## Model` section | Persistent (in charter) |
|
| **2** | Charter Preference | Agent's `charter.md` → `## Model` section | Persistent (in charter) |
|
||||||
| **3** | Task-Aware Auto | Code → sonnet, docs → haiku, visual → opus | Computed per-spawn |
|
| **3** | Task-Aware Auto | Code → sonnet, docs → haiku, visual → opus | Computed per-spawn |
|
||||||
| **4** | Default | `claude-haiku-4.5` | Hardcoded fallback |
|
| **4** | Default | `claude-haiku-4.5` | Hardcoded fallback |
|
||||||
|
|
||||||
**Key principle:** Layer 0 (persistent config) beats everything. If the user said "always use opus" and it was saved to config.json, every agent gets opus regardless of role or task type. This is intentional — the user explicitly chose quality over cost.
|
**Key principle:** Layer 0 (persistent config) beats everything. If the user said "always use opus" and it was saved to config.json, every agent gets opus regardless of role or task type. This is intentional — the user explicitly chose quality over cost.
|
||||||
|
|
||||||
## AGENT WORKFLOW
|
## AGENT WORKFLOW
|
||||||
|
|
||||||
### On Session Start
|
### On Session Start
|
||||||
|
|
||||||
1. READ `.squad/config.json`
|
1. READ `.squad/config.json`
|
||||||
2. CHECK for `defaultModel` field — if present, this is the Layer 0 override for all spawns
|
2. CHECK for `defaultModel` field — if present, this is the Layer 0 override for all spawns
|
||||||
3. CHECK for `agentModelOverrides` field — if present, these are per-agent Layer 0a overrides
|
3. CHECK for `agentModelOverrides` field — if present, these are per-agent Layer 0a overrides
|
||||||
4. STORE both values in session context for the duration
|
4. STORE both values in session context for the duration
|
||||||
|
|
||||||
### On Every Agent Spawn
|
### On Every Agent Spawn
|
||||||
|
|
||||||
1. CHECK Layer 0a: Is there an `agentModelOverrides.{agentName}` in config.json? → Use it.
|
1. CHECK Layer 0a: Is there an `agentModelOverrides.{agentName}` in config.json? → Use it.
|
||||||
2. CHECK Layer 0b: Is there a `defaultModel` in config.json? → Use it.
|
2. CHECK Layer 0b: Is there a `defaultModel` in config.json? → Use it.
|
||||||
3. CHECK Layer 1: Did the user give a session directive? → Use it.
|
3. CHECK Layer 1: Did the user give a session directive? → Use it.
|
||||||
4. CHECK Layer 2: Does the agent's charter have a `## Model` section? → Use it.
|
4. CHECK Layer 2: Does the agent's charter have a `## Model` section? → Use it.
|
||||||
5. CHECK Layer 3: Determine task type:
|
5. CHECK Layer 3: Determine task type:
|
||||||
- Code (implementation, tests, refactoring, bug fixes) → `claude-sonnet-4.6`
|
- Code (implementation, tests, refactoring, bug fixes) → `claude-sonnet-4.6`
|
||||||
- Prompts, agent designs → `claude-sonnet-4.6`
|
- Prompts, agent designs → `claude-sonnet-4.6`
|
||||||
- Visual/design with image analysis → `claude-opus-4.6`
|
- Visual/design with image analysis → `claude-opus-4.6`
|
||||||
- Non-code (docs, planning, triage, changelogs) → `claude-haiku-4.5`
|
- Non-code (docs, planning, triage, changelogs) → `claude-haiku-4.5`
|
||||||
6. FALLBACK Layer 4: `claude-haiku-4.5`
|
6. FALLBACK Layer 4: `claude-haiku-4.5`
|
||||||
7. INCLUDE model in spawn acknowledgment: `🔧 {Name} ({resolved_model}) — {task}`
|
7. INCLUDE model in spawn acknowledgment: `🔧 {Name} ({resolved_model}) — {task}`
|
||||||
|
|
||||||
### When User Sets a Preference
|
### When User Sets a Preference
|
||||||
|
|
||||||
**Trigger phrases:** "always use X", "use X for everything", "switch to X", "default to X"
|
**Trigger phrases:** "always use X", "use X for everything", "switch to X", "default to X"
|
||||||
|
|
||||||
1. VALIDATE the model ID against the catalog (18+ models)
|
1. VALIDATE the model ID against the catalog (18+ models)
|
||||||
2. WRITE `defaultModel` to `.squad/config.json` (merge, don't overwrite)
|
2. WRITE `defaultModel` to `.squad/config.json` (merge, don't overwrite)
|
||||||
3. ACKNOWLEDGE: `✅ Model preference saved: {model} — all future sessions will use this until changed.`
|
3. ACKNOWLEDGE: `✅ Model preference saved: {model} — all future sessions will use this until changed.`
|
||||||
|
|
||||||
**Per-agent trigger:** "use X for {agent}"
|
**Per-agent trigger:** "use X for {agent}"
|
||||||
|
|
||||||
1. VALIDATE model ID
|
1. VALIDATE model ID
|
||||||
2. WRITE to `agentModelOverrides.{agent}` in `.squad/config.json`
|
2. WRITE to `agentModelOverrides.{agent}` in `.squad/config.json`
|
||||||
3. ACKNOWLEDGE: `✅ {Agent} will always use {model} — saved to config.`
|
3. ACKNOWLEDGE: `✅ {Agent} will always use {model} — saved to config.`
|
||||||
|
|
||||||
### When User Clears a Preference
|
### When User Clears a Preference
|
||||||
|
|
||||||
**Trigger phrases:** "switch back to automatic", "clear model preference", "use default models"
|
**Trigger phrases:** "switch back to automatic", "clear model preference", "use default models"
|
||||||
|
|
||||||
1. REMOVE `defaultModel` from `.squad/config.json`
|
1. REMOVE `defaultModel` from `.squad/config.json`
|
||||||
2. ACKNOWLEDGE: `✅ Model preference cleared — returning to automatic selection.`
|
2. ACKNOWLEDGE: `✅ Model preference cleared — returning to automatic selection.`
|
||||||
|
|
||||||
### STOP
|
### STOP
|
||||||
|
|
||||||
After resolving the model and including it in the spawn template, this skill is done. Do NOT:
|
After resolving the model and including it in the spawn template, this skill is done. Do NOT:
|
||||||
- Generate model comparison reports
|
- Generate model comparison reports
|
||||||
- Run benchmarks or speed tests
|
- Run benchmarks or speed tests
|
||||||
- Create new config files (only modify existing `.squad/config.json`)
|
- Create new config files (only modify existing `.squad/config.json`)
|
||||||
- Change the model after spawn (fallback chains handle runtime failures)
|
- Change the model after spawn (fallback chains handle runtime failures)
|
||||||
|
|
||||||
## Config Schema
|
## Config Schema
|
||||||
|
|
||||||
`.squad/config.json` model-related fields:
|
`.squad/config.json` model-related fields:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"defaultModel": "claude-opus-4.6",
|
"defaultModel": "claude-opus-4.6",
|
||||||
"agentModelOverrides": {
|
"agentModelOverrides": {
|
||||||
"fenster": "claude-sonnet-4.6",
|
"fenster": "claude-sonnet-4.6",
|
||||||
"mcmanus": "claude-haiku-4.5"
|
"mcmanus": "claude-haiku-4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `defaultModel` — applies to ALL agents unless overridden by `agentModelOverrides`
|
- `defaultModel` — applies to ALL agents unless overridden by `agentModelOverrides`
|
||||||
- `agentModelOverrides` — per-agent overrides that take priority over `defaultModel`
|
- `agentModelOverrides` — per-agent overrides that take priority over `defaultModel`
|
||||||
- Both fields are optional. When absent, Layers 1-4 apply normally.
|
- Both fields are optional. When absent, Layers 1-4 apply normally.
|
||||||
|
|
||||||
## Fallback Chains
|
## Fallback Chains
|
||||||
|
|
||||||
If a model is unavailable (rate limit, plan restriction), retry within the same tier:
|
If a model is unavailable (rate limit, plan restriction), retry within the same tier:
|
||||||
|
|
||||||
```
|
```
|
||||||
Premium: claude-opus-4.6 → claude-opus-4.6-fast → claude-opus-4.5 → claude-sonnet-4.6
|
Premium: claude-opus-4.6 → claude-opus-4.6-fast → claude-opus-4.5 → claude-sonnet-4.6
|
||||||
Standard: claude-sonnet-4.6 → gpt-5.4 → claude-sonnet-4.5 → gpt-5.3-codex → claude-sonnet-4
|
Standard: claude-sonnet-4.6 → gpt-5.4 → claude-sonnet-4.5 → gpt-5.3-codex → claude-sonnet-4
|
||||||
Fast: claude-haiku-4.5 → gpt-5.1-codex-mini → gpt-4.1 → gpt-5-mini
|
Fast: claude-haiku-4.5 → gpt-5.1-codex-mini → gpt-4.1 → gpt-5-mini
|
||||||
```
|
```
|
||||||
|
|
||||||
**Never fall UP in tier.** A fast task won't land on a premium model via fallback.
|
**Never fall UP in tier.** A fast task won't land on a premium model via fallback.
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
# Skill: nap
|
# Skill: nap
|
||||||
|
|
||||||
> Context hygiene — compress, prune, archive .squad/ state
|
> Context hygiene — compress, prune, archive .squad/ state
|
||||||
|
|
||||||
## What It Does
|
## What It Does
|
||||||
|
|
||||||
Reclaims context window budget by compressing agent histories, pruning old logs,
|
Reclaims context window budget by compressing agent histories, pruning old logs,
|
||||||
archiving stale decisions, and cleaning orphaned inbox files.
|
archiving stale decisions, and cleaning orphaned inbox files.
|
||||||
|
|
||||||
## When To Use
|
## When To Use
|
||||||
|
|
||||||
- Before heavy fan-out work (many agents will spawn)
|
- Before heavy fan-out work (many agents will spawn)
|
||||||
- When history.md files exceed 15KB
|
- When history.md files exceed 15KB
|
||||||
- When .squad/ total size exceeds 1MB
|
- When .squad/ total size exceeds 1MB
|
||||||
- After long-running sessions or sprints
|
- After long-running sessions or sprints
|
||||||
|
|
||||||
## Invocation
|
## Invocation
|
||||||
|
|
||||||
- CLI: `squad nap` / `squad nap --deep` / `squad nap --dry-run`
|
- CLI: `squad nap` / `squad nap --deep` / `squad nap --dry-run`
|
||||||
- REPL: `/nap` / `/nap --dry-run` / `/nap --deep`
|
- REPL: `/nap` / `/nap --dry-run` / `/nap --deep`
|
||||||
|
|
||||||
## Confidence
|
## Confidence
|
||||||
|
|
||||||
medium — Confirmed by team vote (4-1) and initial implementation
|
medium — Confirmed by team vote (4-1) and initial implementation
|
||||||
|
|||||||
@@ -1,57 +1,57 @@
|
|||||||
# Personal Squad — Skill Document
|
# Personal Squad — Skill Document
|
||||||
|
|
||||||
## What is a Personal Squad?
|
## What is a Personal Squad?
|
||||||
|
|
||||||
A personal squad is a user-level collection of AI agents that travel with you across projects. Unlike project agents (defined in a project's `.squad/` directory), personal agents live in your global config directory and are automatically discovered when you start a squad session.
|
A personal squad is a user-level collection of AI agents that travel with you across projects. Unlike project agents (defined in a project's `.squad/` directory), personal agents live in your global config directory and are automatically discovered when you start a squad session.
|
||||||
|
|
||||||
## Directory Structure
|
## Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
~/.config/squad/personal-squad/ # Linux/macOS
|
~/.config/squad/personal-squad/ # Linux/macOS
|
||||||
%APPDATA%/squad/personal-squad/ # Windows
|
%APPDATA%/squad/personal-squad/ # Windows
|
||||||
├── agents/
|
├── agents/
|
||||||
│ ├── {agent-name}/
|
│ ├── {agent-name}/
|
||||||
│ │ ├── charter.md
|
│ │ ├── charter.md
|
||||||
│ │ └── history.md
|
│ │ └── history.md
|
||||||
│ └── ...
|
│ └── ...
|
||||||
└── config.json # Optional: personal squad config
|
└── config.json # Optional: personal squad config
|
||||||
```
|
```
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
1. **Ambient Discovery:** When Squad starts a session, it checks for a personal squad directory
|
1. **Ambient Discovery:** When Squad starts a session, it checks for a personal squad directory
|
||||||
2. **Merge:** Personal agents are merged into the session cast alongside project agents
|
2. **Merge:** Personal agents are merged into the session cast alongside project agents
|
||||||
3. **Ghost Protocol:** Personal agents can read project state but not write to it
|
3. **Ghost Protocol:** Personal agents can read project state but not write to it
|
||||||
4. **Kill Switch:** Set `SQUAD_NO_PERSONAL=1` to disable ambient discovery
|
4. **Kill Switch:** Set `SQUAD_NO_PERSONAL=1` to disable ambient discovery
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
- `squad personal init` — Bootstrap a personal squad directory
|
- `squad personal init` — Bootstrap a personal squad directory
|
||||||
- `squad personal list` — List your personal agents
|
- `squad personal list` — List your personal agents
|
||||||
- `squad personal add {name} --role {role}` — Add a personal agent
|
- `squad personal add {name} --role {role}` — Add a personal agent
|
||||||
- `squad personal remove {name}` — Remove a personal agent
|
- `squad personal remove {name}` — Remove a personal agent
|
||||||
- `squad cast` — Show the current session cast (project + personal)
|
- `squad cast` — Show the current session cast (project + personal)
|
||||||
|
|
||||||
## Ghost Protocol
|
## Ghost Protocol
|
||||||
|
|
||||||
See `templates/ghost-protocol.md` for the full rules. Key points:
|
See `templates/ghost-protocol.md` for the full rules. Key points:
|
||||||
- Personal agents advise; project agents execute
|
- Personal agents advise; project agents execute
|
||||||
- No writes to project `.squad/` state
|
- No writes to project `.squad/` state
|
||||||
- Transparent origin tagging in logs
|
- Transparent origin tagging in logs
|
||||||
- Project agents take precedence on conflicts
|
- Project agents take precedence on conflicts
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Optional `config.json` in the personal squad directory:
|
Optional `config.json` in the personal squad directory:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"defaultModel": "auto",
|
"defaultModel": "auto",
|
||||||
"ghostProtocol": true,
|
"ghostProtocol": true,
|
||||||
"agents": {}
|
"agents": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
- `SQUAD_NO_PERSONAL` — Set to any value to disable personal squad discovery
|
- `SQUAD_NO_PERSONAL` — Set to any value to disable personal squad discovery
|
||||||
- `SQUAD_PERSONAL_DIR` — Override the default personal squad directory path
|
- `SQUAD_PERSONAL_DIR` — Override the default personal squad directory path
|
||||||
|
|||||||
@@ -1,56 +1,56 @@
|
|||||||
---
|
---
|
||||||
name: "project-conventions"
|
name: "project-conventions"
|
||||||
description: "Core conventions and patterns for this codebase"
|
description: "Core conventions and patterns for this codebase"
|
||||||
domain: "project-conventions"
|
domain: "project-conventions"
|
||||||
confidence: "medium"
|
confidence: "medium"
|
||||||
source: "template"
|
source: "template"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
> **This is a starter template.** Replace the placeholder patterns below with your actual project conventions. Skills train agents on codebase-specific practices — accurate documentation here improves agent output quality.
|
> **This is a starter template.** Replace the placeholder patterns below with your actual project conventions. Skills train agents on codebase-specific practices — accurate documentation here improves agent output quality.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### [Pattern Name]
|
### [Pattern Name]
|
||||||
|
|
||||||
Describe a key convention or practice used in this codebase. Be specific about what to do and why.
|
Describe a key convention or practice used in this codebase. Be specific about what to do and why.
|
||||||
|
|
||||||
### Error Handling
|
### Error Handling
|
||||||
|
|
||||||
<!-- Example: How does your project handle errors? -->
|
<!-- Example: How does your project handle errors? -->
|
||||||
<!-- - Use try/catch with specific error types? -->
|
<!-- - Use try/catch with specific error types? -->
|
||||||
<!-- - Log to a specific service? -->
|
<!-- - Log to a specific service? -->
|
||||||
<!-- - Return error objects vs throwing? -->
|
<!-- - Return error objects vs throwing? -->
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
<!-- Example: What test framework? Where do tests live? How to run them? -->
|
<!-- Example: What test framework? Where do tests live? How to run them? -->
|
||||||
<!-- - Test framework: Jest/Vitest/node:test/etc. -->
|
<!-- - Test framework: Jest/Vitest/node:test/etc. -->
|
||||||
<!-- - Test location: test/, __tests__/, *.test.ts, etc. -->
|
<!-- - Test location: test/, __tests__/, *.test.ts, etc. -->
|
||||||
<!-- - Run command: npm test, etc. -->
|
<!-- - Run command: npm test, etc. -->
|
||||||
|
|
||||||
### Code Style
|
### Code Style
|
||||||
|
|
||||||
<!-- Example: Linting, formatting, naming conventions -->
|
<!-- Example: Linting, formatting, naming conventions -->
|
||||||
<!-- - Linter: ESLint config? -->
|
<!-- - Linter: ESLint config? -->
|
||||||
<!-- - Formatter: Prettier? -->
|
<!-- - Formatter: Prettier? -->
|
||||||
<!-- - Naming: camelCase, snake_case, etc.? -->
|
<!-- - Naming: camelCase, snake_case, etc.? -->
|
||||||
|
|
||||||
### File Structure
|
### File Structure
|
||||||
|
|
||||||
<!-- Example: How is the project organized? -->
|
<!-- Example: How is the project organized? -->
|
||||||
<!-- - src/ — Source code -->
|
<!-- - src/ — Source code -->
|
||||||
<!-- - test/ — Tests -->
|
<!-- - test/ — Tests -->
|
||||||
<!-- - docs/ — Documentation -->
|
<!-- - docs/ — Documentation -->
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
```
|
```
|
||||||
// Add code examples that demonstrate your conventions
|
// Add code examples that demonstrate your conventions
|
||||||
```
|
```
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
<!-- List things to avoid in this codebase -->
|
<!-- List things to avoid in this codebase -->
|
||||||
- **[Anti-pattern]** — Explanation of what not to do and why.
|
- **[Anti-pattern]** — Explanation of what not to do and why.
|
||||||
|
|||||||
@@ -1,423 +1,423 @@
|
|||||||
---
|
---
|
||||||
name: "release-process"
|
name: "release-process"
|
||||||
description: "Step-by-step release checklist for Squad — prevents v0.8.22-style disasters"
|
description: "Step-by-step release checklist for Squad — prevents v0.8.22-style disasters"
|
||||||
domain: "release-management"
|
domain: "release-management"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "team-decision"
|
source: "team-decision"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
This is the **definitive release runbook** for Squad. Born from the v0.8.22 release disaster (4-part semver mangled by npm, draft release never triggered publish, wrong NPM_TOKEN type, 6+ hours of broken `latest` dist-tag).
|
This is the **definitive release runbook** for Squad. Born from the v0.8.22 release disaster (4-part semver mangled by npm, draft release never triggered publish, wrong NPM_TOKEN type, 6+ hours of broken `latest` dist-tag).
|
||||||
|
|
||||||
**Rule:** No agent releases Squad without following this checklist. No exceptions. No improvisation.
|
**Rule:** No agent releases Squad without following this checklist. No exceptions. No improvisation.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Pre-Release Validation
|
## Pre-Release Validation
|
||||||
|
|
||||||
Before starting ANY release work, validate the following:
|
Before starting ANY release work, validate the following:
|
||||||
|
|
||||||
### 1. Version Number Validation
|
### 1. Version Number Validation
|
||||||
|
|
||||||
**Rule:** Only 3-part semver (major.minor.patch) or prerelease (major.minor.patch-tag.N) are valid. 4-part versions (0.8.21.4) are NOT valid semver and npm will mangle them.
|
**Rule:** Only 3-part semver (major.minor.patch) or prerelease (major.minor.patch-tag.N) are valid. 4-part versions (0.8.21.4) are NOT valid semver and npm will mangle them.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check version is valid semver
|
# Check version is valid semver
|
||||||
node -p "require('semver').valid('0.8.22')"
|
node -p "require('semver').valid('0.8.22')"
|
||||||
# Output: '0.8.22' = valid
|
# Output: '0.8.22' = valid
|
||||||
# Output: null = INVALID, STOP
|
# Output: null = INVALID, STOP
|
||||||
|
|
||||||
# For prerelease versions
|
# For prerelease versions
|
||||||
node -p "require('semver').valid('0.8.23-preview.1')"
|
node -p "require('semver').valid('0.8.23-preview.1')"
|
||||||
# Output: '0.8.23-preview.1' = valid
|
# Output: '0.8.23-preview.1' = valid
|
||||||
```
|
```
|
||||||
|
|
||||||
**If `semver.valid()` returns `null`:** STOP. Fix the version. Do NOT proceed.
|
**If `semver.valid()` returns `null`:** STOP. Fix the version. Do NOT proceed.
|
||||||
|
|
||||||
### 2. NPM_TOKEN Verification
|
### 2. NPM_TOKEN Verification
|
||||||
|
|
||||||
**Rule:** NPM_TOKEN must be an **Automation token** (no 2FA required). User tokens with 2FA will fail in CI with EOTP errors.
|
**Rule:** NPM_TOKEN must be an **Automation token** (no 2FA required). User tokens with 2FA will fail in CI with EOTP errors.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check token type (requires npm CLI authenticated)
|
# Check token type (requires npm CLI authenticated)
|
||||||
npm token list
|
npm token list
|
||||||
```
|
```
|
||||||
|
|
||||||
Look for:
|
Look for:
|
||||||
- ✅ `read-write` tokens with NO 2FA requirement = Automation token (correct)
|
- ✅ `read-write` tokens with NO 2FA requirement = Automation token (correct)
|
||||||
- ❌ Tokens requiring OTP = User token (WRONG, will fail in CI)
|
- ❌ Tokens requiring OTP = User token (WRONG, will fail in CI)
|
||||||
|
|
||||||
**How to create an Automation token:**
|
**How to create an Automation token:**
|
||||||
1. Go to npmjs.com → Settings → Access Tokens
|
1. Go to npmjs.com → Settings → Access Tokens
|
||||||
2. Click "Generate New Token"
|
2. Click "Generate New Token"
|
||||||
3. Select **"Automation"** (NOT "Publish")
|
3. Select **"Automation"** (NOT "Publish")
|
||||||
4. Copy token and save as GitHub secret: `NPM_TOKEN`
|
4. Copy token and save as GitHub secret: `NPM_TOKEN`
|
||||||
|
|
||||||
**If using a User token:** STOP. Create an Automation token first.
|
**If using a User token:** STOP. Create an Automation token first.
|
||||||
|
|
||||||
### 3. Branch and Tag State
|
### 3. Branch and Tag State
|
||||||
|
|
||||||
**Rule:** Release from `main` branch. Ensure clean state, no uncommitted changes, latest from origin.
|
**Rule:** Release from `main` branch. Ensure clean state, no uncommitted changes, latest from origin.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Ensure on main and clean
|
# Ensure on main and clean
|
||||||
git checkout main
|
git checkout main
|
||||||
git pull origin main
|
git pull origin main
|
||||||
git status # Should show: "nothing to commit, working tree clean"
|
git status # Should show: "nothing to commit, working tree clean"
|
||||||
|
|
||||||
# Check tag doesn't already exist
|
# Check tag doesn't already exist
|
||||||
git tag -l "v0.8.22"
|
git tag -l "v0.8.22"
|
||||||
# Output should be EMPTY. If tag exists, release already done or collision.
|
# Output should be EMPTY. If tag exists, release already done or collision.
|
||||||
```
|
```
|
||||||
|
|
||||||
**If tag exists:** STOP. Either release was already done, or there's a collision. Investigate before proceeding.
|
**If tag exists:** STOP. Either release was already done, or there's a collision. Investigate before proceeding.
|
||||||
|
|
||||||
### 4. Disable bump-build.mjs
|
### 4. Disable bump-build.mjs
|
||||||
|
|
||||||
**Rule:** `bump-build.mjs` is for dev builds ONLY. It must NOT run during release builds (it increments build numbers, creating 4-part versions).
|
**Rule:** `bump-build.mjs` is for dev builds ONLY. It must NOT run during release builds (it increments build numbers, creating 4-part versions).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Set env var to skip bump-build.mjs
|
# Set env var to skip bump-build.mjs
|
||||||
export SKIP_BUILD_BUMP=1
|
export SKIP_BUILD_BUMP=1
|
||||||
|
|
||||||
# Verify it's set
|
# Verify it's set
|
||||||
echo $SKIP_BUILD_BUMP
|
echo $SKIP_BUILD_BUMP
|
||||||
# Output: 1
|
# Output: 1
|
||||||
```
|
```
|
||||||
|
|
||||||
**For Windows PowerShell:**
|
**For Windows PowerShell:**
|
||||||
```powershell
|
```powershell
|
||||||
$env:SKIP_BUILD_BUMP = "1"
|
$env:SKIP_BUILD_BUMP = "1"
|
||||||
```
|
```
|
||||||
|
|
||||||
**If not set:** `bump-build.mjs` will run and mutate versions. This causes disasters (see v0.8.22).
|
**If not set:** `bump-build.mjs` will run and mutate versions. This causes disasters (see v0.8.22).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Release Workflow
|
## Release Workflow
|
||||||
|
|
||||||
### Step 1: Version Bump
|
### Step 1: Version Bump
|
||||||
|
|
||||||
Update version in all 3 package.json files (root + both workspaces) in lockstep.
|
Update version in all 3 package.json files (root + both workspaces) in lockstep.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Set target version (no 'v' prefix)
|
# Set target version (no 'v' prefix)
|
||||||
VERSION="0.8.22"
|
VERSION="0.8.22"
|
||||||
|
|
||||||
# Validate it's valid semver BEFORE proceeding
|
# Validate it's valid semver BEFORE proceeding
|
||||||
node -p "require('semver').valid('$VERSION')"
|
node -p "require('semver').valid('$VERSION')"
|
||||||
# Must output the version string, NOT null
|
# Must output the version string, NOT null
|
||||||
|
|
||||||
# Update all 3 package.json files
|
# Update all 3 package.json files
|
||||||
npm version $VERSION --workspaces --include-workspace-root --no-git-tag-version
|
npm version $VERSION --workspaces --include-workspace-root --no-git-tag-version
|
||||||
|
|
||||||
# Verify all 3 match
|
# Verify all 3 match
|
||||||
grep '"version"' package.json packages/squad-sdk/package.json packages/squad-cli/package.json
|
grep '"version"' package.json packages/squad-sdk/package.json packages/squad-cli/package.json
|
||||||
# All 3 should show: "version": "0.8.22"
|
# All 3 should show: "version": "0.8.22"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Checkpoint:** All 3 package.json files have identical versions. Run `semver.valid()` one more time to be sure.
|
**Checkpoint:** All 3 package.json files have identical versions. Run `semver.valid()` one more time to be sure.
|
||||||
|
|
||||||
### Step 2: Commit and Tag
|
### Step 2: Commit and Tag
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Commit version bump
|
# Commit version bump
|
||||||
git add package.json packages/squad-sdk/package.json packages/squad-cli/package.json
|
git add package.json packages/squad-sdk/package.json packages/squad-cli/package.json
|
||||||
git commit -m "chore: bump version to $VERSION
|
git commit -m "chore: bump version to $VERSION
|
||||||
|
|
||||||
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>"
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>"
|
||||||
|
|
||||||
# Create tag (with 'v' prefix)
|
# Create tag (with 'v' prefix)
|
||||||
git tag -a "v$VERSION" -m "Release v$VERSION"
|
git tag -a "v$VERSION" -m "Release v$VERSION"
|
||||||
|
|
||||||
# Push commit and tag
|
# Push commit and tag
|
||||||
git push origin main
|
git push origin main
|
||||||
git push origin "v$VERSION"
|
git push origin "v$VERSION"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Checkpoint:** Tag created and pushed. Verify with `git tag -l "v$VERSION"`.
|
**Checkpoint:** Tag created and pushed. Verify with `git tag -l "v$VERSION"`.
|
||||||
|
|
||||||
### Step 3: Create GitHub Release
|
### Step 3: Create GitHub Release
|
||||||
|
|
||||||
**CRITICAL:** Release must be **published**, NOT draft. Draft releases don't trigger `publish.yml` workflow.
|
**CRITICAL:** Release must be **published**, NOT draft. Draft releases don't trigger `publish.yml` workflow.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create GitHub Release (NOT draft)
|
# Create GitHub Release (NOT draft)
|
||||||
gh release create "v$VERSION" \
|
gh release create "v$VERSION" \
|
||||||
--title "v$VERSION" \
|
--title "v$VERSION" \
|
||||||
--notes "Release notes go here" \
|
--notes "Release notes go here" \
|
||||||
--latest
|
--latest
|
||||||
|
|
||||||
# Verify release is PUBLISHED (not draft)
|
# Verify release is PUBLISHED (not draft)
|
||||||
gh release view "v$VERSION"
|
gh release view "v$VERSION"
|
||||||
# Output should NOT contain "(draft)"
|
# Output should NOT contain "(draft)"
|
||||||
```
|
```
|
||||||
|
|
||||||
**If output contains `(draft)`:** STOP. Delete the release and recreate without `--draft` flag.
|
**If output contains `(draft)`:** STOP. Delete the release and recreate without `--draft` flag.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# If you accidentally created a draft, fix it:
|
# If you accidentally created a draft, fix it:
|
||||||
gh release edit "v$VERSION" --draft=false
|
gh release edit "v$VERSION" --draft=false
|
||||||
```
|
```
|
||||||
|
|
||||||
**Checkpoint:** Release is published (NOT draft). The `release: published` event fired and triggered `publish.yml`.
|
**Checkpoint:** Release is published (NOT draft). The `release: published` event fired and triggered `publish.yml`.
|
||||||
|
|
||||||
### Step 4: Monitor Workflow
|
### Step 4: Monitor Workflow
|
||||||
|
|
||||||
The `publish.yml` workflow should start automatically within 10 seconds of release creation.
|
The `publish.yml` workflow should start automatically within 10 seconds of release creation.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Watch workflow runs
|
# Watch workflow runs
|
||||||
gh run list --workflow=publish.yml --limit 1
|
gh run list --workflow=publish.yml --limit 1
|
||||||
|
|
||||||
# Get detailed status
|
# Get detailed status
|
||||||
gh run view --log
|
gh run view --log
|
||||||
```
|
```
|
||||||
|
|
||||||
**Expected flow:**
|
**Expected flow:**
|
||||||
1. `publish-sdk` job runs → publishes `@bradygaster/squad-sdk`
|
1. `publish-sdk` job runs → publishes `@bradygaster/squad-sdk`
|
||||||
2. Verify step runs with retry loop (up to 5 attempts, 15s interval) to confirm SDK on npm registry
|
2. Verify step runs with retry loop (up to 5 attempts, 15s interval) to confirm SDK on npm registry
|
||||||
3. `publish-cli` job runs → publishes `@bradygaster/squad-cli`
|
3. `publish-cli` job runs → publishes `@bradygaster/squad-cli`
|
||||||
4. Verify step runs with retry loop to confirm CLI on npm registry
|
4. Verify step runs with retry loop to confirm CLI on npm registry
|
||||||
|
|
||||||
**If workflow fails:** Check the logs. Common issues:
|
**If workflow fails:** Check the logs. Common issues:
|
||||||
- EOTP error = wrong NPM_TOKEN type (use Automation token)
|
- EOTP error = wrong NPM_TOKEN type (use Automation token)
|
||||||
- Verify step timeout = npm propagation delay (retry loop should handle this, but propagation can take up to 2 minutes in rare cases)
|
- Verify step timeout = npm propagation delay (retry loop should handle this, but propagation can take up to 2 minutes in rare cases)
|
||||||
- Version mismatch = package.json version doesn't match tag
|
- Version mismatch = package.json version doesn't match tag
|
||||||
|
|
||||||
**Checkpoint:** Both jobs succeeded. Workflow shows green checkmarks.
|
**Checkpoint:** Both jobs succeeded. Workflow shows green checkmarks.
|
||||||
|
|
||||||
### Step 5: Verify npm Publication
|
### Step 5: Verify npm Publication
|
||||||
|
|
||||||
Manually verify both packages are on npm with correct `latest` dist-tag.
|
Manually verify both packages are on npm with correct `latest` dist-tag.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check SDK
|
# Check SDK
|
||||||
npm view @bradygaster/squad-sdk version
|
npm view @bradygaster/squad-sdk version
|
||||||
# Output: 0.8.22
|
# Output: 0.8.22
|
||||||
|
|
||||||
npm dist-tag ls @bradygaster/squad-sdk
|
npm dist-tag ls @bradygaster/squad-sdk
|
||||||
# Output should show: latest: 0.8.22
|
# Output should show: latest: 0.8.22
|
||||||
|
|
||||||
# Check CLI
|
# Check CLI
|
||||||
npm view @bradygaster/squad-cli version
|
npm view @bradygaster/squad-cli version
|
||||||
# Output: 0.8.22
|
# Output: 0.8.22
|
||||||
|
|
||||||
npm dist-tag ls @bradygaster/squad-cli
|
npm dist-tag ls @bradygaster/squad-cli
|
||||||
# Output should show: latest: 0.8.22
|
# Output should show: latest: 0.8.22
|
||||||
```
|
```
|
||||||
|
|
||||||
**If versions don't match:** Something went wrong. Check workflow logs. DO NOT proceed with GitHub Release announcement until npm is correct.
|
**If versions don't match:** Something went wrong. Check workflow logs. DO NOT proceed with GitHub Release announcement until npm is correct.
|
||||||
|
|
||||||
**Checkpoint:** Both packages show correct version. `latest` dist-tags point to the new version.
|
**Checkpoint:** Both packages show correct version. `latest` dist-tags point to the new version.
|
||||||
|
|
||||||
### Step 6: Test Installation
|
### Step 6: Test Installation
|
||||||
|
|
||||||
Verify packages can be installed from npm (real-world smoke test).
|
Verify packages can be installed from npm (real-world smoke test).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create temp directory
|
# Create temp directory
|
||||||
mkdir /tmp/squad-release-test && cd /tmp/squad-release-test
|
mkdir /tmp/squad-release-test && cd /tmp/squad-release-test
|
||||||
|
|
||||||
# Test SDK installation
|
# Test SDK installation
|
||||||
npm init -y
|
npm init -y
|
||||||
npm install @bradygaster/squad-sdk
|
npm install @bradygaster/squad-sdk
|
||||||
node -p "require('@bradygaster/squad-sdk/package.json').version"
|
node -p "require('@bradygaster/squad-sdk/package.json').version"
|
||||||
# Output: 0.8.22
|
# Output: 0.8.22
|
||||||
|
|
||||||
# Test CLI installation
|
# Test CLI installation
|
||||||
npm install -g @bradygaster/squad-cli
|
npm install -g @bradygaster/squad-cli
|
||||||
squad --version
|
squad --version
|
||||||
# Output: 0.8.22
|
# Output: 0.8.22
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
cd -
|
cd -
|
||||||
rm -rf /tmp/squad-release-test
|
rm -rf /tmp/squad-release-test
|
||||||
```
|
```
|
||||||
|
|
||||||
**If installation fails:** npm registry issue or package metadata corruption. DO NOT announce release until this works.
|
**If installation fails:** npm registry issue or package metadata corruption. DO NOT announce release until this works.
|
||||||
|
|
||||||
**Checkpoint:** Both packages install cleanly. Versions match.
|
**Checkpoint:** Both packages install cleanly. Versions match.
|
||||||
|
|
||||||
### Step 7: Sync dev to Next Preview
|
### Step 7: Sync dev to Next Preview
|
||||||
|
|
||||||
After main release, sync dev to the next preview version.
|
After main release, sync dev to the next preview version.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Checkout dev
|
# Checkout dev
|
||||||
git checkout dev
|
git checkout dev
|
||||||
git pull origin dev
|
git pull origin dev
|
||||||
|
|
||||||
# Bump to next preview version (e.g., 0.8.23-preview.1)
|
# Bump to next preview version (e.g., 0.8.23-preview.1)
|
||||||
NEXT_VERSION="0.8.23-preview.1"
|
NEXT_VERSION="0.8.23-preview.1"
|
||||||
|
|
||||||
# Validate semver
|
# Validate semver
|
||||||
node -p "require('semver').valid('$NEXT_VERSION')"
|
node -p "require('semver').valid('$NEXT_VERSION')"
|
||||||
# Must output the version string, NOT null
|
# Must output the version string, NOT null
|
||||||
|
|
||||||
# Update all 3 package.json files
|
# Update all 3 package.json files
|
||||||
npm version $NEXT_VERSION --workspaces --include-workspace-root --no-git-tag-version
|
npm version $NEXT_VERSION --workspaces --include-workspace-root --no-git-tag-version
|
||||||
|
|
||||||
# Commit
|
# Commit
|
||||||
git add package.json packages/squad-sdk/package.json packages/squad-cli/package.json
|
git add package.json packages/squad-sdk/package.json packages/squad-cli/package.json
|
||||||
git commit -m "chore: bump dev to $NEXT_VERSION
|
git commit -m "chore: bump dev to $NEXT_VERSION
|
||||||
|
|
||||||
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>"
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>"
|
||||||
|
|
||||||
# Push
|
# Push
|
||||||
git push origin dev
|
git push origin dev
|
||||||
```
|
```
|
||||||
|
|
||||||
**Checkpoint:** dev branch now shows next preview version. Future dev builds will publish to `@preview` dist-tag.
|
**Checkpoint:** dev branch now shows next preview version. Future dev builds will publish to `@preview` dist-tag.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Manual Publish (Fallback)
|
## Manual Publish (Fallback)
|
||||||
|
|
||||||
If `publish.yml` workflow fails or needs to be bypassed, use `workflow_dispatch` to manually trigger publish.
|
If `publish.yml` workflow fails or needs to be bypassed, use `workflow_dispatch` to manually trigger publish.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Trigger manual publish
|
# Trigger manual publish
|
||||||
gh workflow run publish.yml -f version="0.8.22"
|
gh workflow run publish.yml -f version="0.8.22"
|
||||||
|
|
||||||
# Monitor the run
|
# Monitor the run
|
||||||
gh run watch
|
gh run watch
|
||||||
```
|
```
|
||||||
|
|
||||||
**Rule:** Only use this if automated publish failed. Always investigate why automation failed and fix it for next release.
|
**Rule:** Only use this if automated publish failed. Always investigate why automation failed and fix it for next release.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Rollback Procedure
|
## Rollback Procedure
|
||||||
|
|
||||||
If a release is broken and needs to be rolled back:
|
If a release is broken and needs to be rolled back:
|
||||||
|
|
||||||
### 1. Unpublish from npm (Nuclear Option)
|
### 1. Unpublish from npm (Nuclear Option)
|
||||||
|
|
||||||
**WARNING:** npm unpublish is time-limited (24 hours) and leaves the version slot burned. Only use if version is critically broken.
|
**WARNING:** npm unpublish is time-limited (24 hours) and leaves the version slot burned. Only use if version is critically broken.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Unpublish (requires npm owner privileges)
|
# Unpublish (requires npm owner privileges)
|
||||||
npm unpublish @bradygaster/squad-sdk@0.8.22
|
npm unpublish @bradygaster/squad-sdk@0.8.22
|
||||||
npm unpublish @bradygaster/squad-cli@0.8.22
|
npm unpublish @bradygaster/squad-cli@0.8.22
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Deprecate on npm (Preferred)
|
### 2. Deprecate on npm (Preferred)
|
||||||
|
|
||||||
**Preferred approach:** Mark version as deprecated, publish a hotfix.
|
**Preferred approach:** Mark version as deprecated, publish a hotfix.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Deprecate broken version
|
# Deprecate broken version
|
||||||
npm deprecate @bradygaster/squad-sdk@0.8.22 "Broken release, use 0.8.22.1 instead"
|
npm deprecate @bradygaster/squad-sdk@0.8.22 "Broken release, use 0.8.22.1 instead"
|
||||||
npm deprecate @bradygaster/squad-cli@0.8.22 "Broken release, use 0.8.22.1 instead"
|
npm deprecate @bradygaster/squad-cli@0.8.22 "Broken release, use 0.8.22.1 instead"
|
||||||
|
|
||||||
# Publish hotfix version
|
# Publish hotfix version
|
||||||
# (Follow this runbook with version 0.8.22.1)
|
# (Follow this runbook with version 0.8.22.1)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Delete GitHub Release and Tag
|
### 3. Delete GitHub Release and Tag
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Delete GitHub Release
|
# Delete GitHub Release
|
||||||
gh release delete "v0.8.22" --yes
|
gh release delete "v0.8.22" --yes
|
||||||
|
|
||||||
# Delete tag locally and remotely
|
# Delete tag locally and remotely
|
||||||
git tag -d "v0.8.22"
|
git tag -d "v0.8.22"
|
||||||
git push origin --delete "v0.8.22"
|
git push origin --delete "v0.8.22"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Revert Commit on main
|
### 4. Revert Commit on main
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Revert version bump commit
|
# Revert version bump commit
|
||||||
git checkout main
|
git checkout main
|
||||||
git revert HEAD
|
git revert HEAD
|
||||||
git push origin main
|
git push origin main
|
||||||
```
|
```
|
||||||
|
|
||||||
**Checkpoint:** Tag and release deleted. main branch reverted. npm packages deprecated or unpublished.
|
**Checkpoint:** Tag and release deleted. main branch reverted. npm packages deprecated or unpublished.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Common Failure Modes
|
## Common Failure Modes
|
||||||
|
|
||||||
### EOTP Error (npm OTP Required)
|
### EOTP Error (npm OTP Required)
|
||||||
|
|
||||||
**Symptom:** Workflow fails with `EOTP` error.
|
**Symptom:** Workflow fails with `EOTP` error.
|
||||||
**Root cause:** NPM_TOKEN is a User token with 2FA enabled. CI can't provide OTP.
|
**Root cause:** NPM_TOKEN is a User token with 2FA enabled. CI can't provide OTP.
|
||||||
**Fix:** Replace NPM_TOKEN with an Automation token (no 2FA). See "NPM_TOKEN Verification" above.
|
**Fix:** Replace NPM_TOKEN with an Automation token (no 2FA). See "NPM_TOKEN Verification" above.
|
||||||
|
|
||||||
### Verify Step 404 (npm Propagation Delay)
|
### Verify Step 404 (npm Propagation Delay)
|
||||||
|
|
||||||
**Symptom:** Verify step fails with 404 even though publish succeeded.
|
**Symptom:** Verify step fails with 404 even though publish succeeded.
|
||||||
**Root cause:** npm registry propagation delay (5-30 seconds).
|
**Root cause:** npm registry propagation delay (5-30 seconds).
|
||||||
**Fix:** Verify step now has retry loop (5 attempts, 15s interval). Should auto-resolve. If not, wait 2 minutes and re-run workflow.
|
**Fix:** Verify step now has retry loop (5 attempts, 15s interval). Should auto-resolve. If not, wait 2 minutes and re-run workflow.
|
||||||
|
|
||||||
### Version Mismatch (package.json ≠ tag)
|
### Version Mismatch (package.json ≠ tag)
|
||||||
|
|
||||||
**Symptom:** Verify step fails with "Package version (X) does not match target version (Y)".
|
**Symptom:** Verify step fails with "Package version (X) does not match target version (Y)".
|
||||||
**Root cause:** package.json version doesn't match the tag version.
|
**Root cause:** package.json version doesn't match the tag version.
|
||||||
**Fix:** Ensure all 3 package.json files were updated in Step 1. Re-run `npm version` if needed.
|
**Fix:** Ensure all 3 package.json files were updated in Step 1. Re-run `npm version` if needed.
|
||||||
|
|
||||||
### 4-Part Version Mangled by npm
|
### 4-Part Version Mangled by npm
|
||||||
|
|
||||||
**Symptom:** Published version on npm doesn't match package.json (e.g., 0.8.21.4 became 0.8.2-1.4).
|
**Symptom:** Published version on npm doesn't match package.json (e.g., 0.8.21.4 became 0.8.2-1.4).
|
||||||
**Root cause:** 4-part versions are NOT valid semver. npm's parser misinterprets them.
|
**Root cause:** 4-part versions are NOT valid semver. npm's parser misinterprets them.
|
||||||
**Fix:** NEVER use 4-part versions. Only 3-part (0.8.22) or prerelease (0.8.23-preview.1). Run `semver.valid()` before ANY commit.
|
**Fix:** NEVER use 4-part versions. Only 3-part (0.8.22) or prerelease (0.8.23-preview.1). Run `semver.valid()` before ANY commit.
|
||||||
|
|
||||||
### Draft Release Didn't Trigger Workflow
|
### Draft Release Didn't Trigger Workflow
|
||||||
|
|
||||||
**Symptom:** Release created but `publish.yml` never ran.
|
**Symptom:** Release created but `publish.yml` never ran.
|
||||||
**Root cause:** Release was created as a draft. Draft releases don't emit `release: published` event.
|
**Root cause:** Release was created as a draft. Draft releases don't emit `release: published` event.
|
||||||
**Fix:** Edit release and change to published: `gh release edit "v$VERSION" --draft=false`. Workflow should trigger immediately.
|
**Fix:** Edit release and change to published: `gh release edit "v$VERSION" --draft=false`. Workflow should trigger immediately.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Validation Checklist
|
## Validation Checklist
|
||||||
|
|
||||||
Before starting ANY release, confirm:
|
Before starting ANY release, confirm:
|
||||||
|
|
||||||
- [ ] Version is valid semver: `node -p "require('semver').valid('VERSION')"` returns the version string (NOT null)
|
- [ ] Version is valid semver: `node -p "require('semver').valid('VERSION')"` returns the version string (NOT null)
|
||||||
- [ ] NPM_TOKEN is an Automation token (no 2FA): `npm token list` shows `read-write` without OTP requirement
|
- [ ] NPM_TOKEN is an Automation token (no 2FA): `npm token list` shows `read-write` without OTP requirement
|
||||||
- [ ] Branch is clean: `git status` shows "nothing to commit, working tree clean"
|
- [ ] Branch is clean: `git status` shows "nothing to commit, working tree clean"
|
||||||
- [ ] Tag doesn't exist: `git tag -l "vVERSION"` returns empty
|
- [ ] Tag doesn't exist: `git tag -l "vVERSION"` returns empty
|
||||||
- [ ] `SKIP_BUILD_BUMP=1` is set: `echo $SKIP_BUILD_BUMP` returns `1`
|
- [ ] `SKIP_BUILD_BUMP=1` is set: `echo $SKIP_BUILD_BUMP` returns `1`
|
||||||
|
|
||||||
Before creating GitHub Release:
|
Before creating GitHub Release:
|
||||||
|
|
||||||
- [ ] All 3 package.json files have matching versions: `grep '"version"' package.json packages/*/package.json`
|
- [ ] All 3 package.json files have matching versions: `grep '"version"' package.json packages/*/package.json`
|
||||||
- [ ] Commit is pushed: `git log origin/main..main` returns empty
|
- [ ] Commit is pushed: `git log origin/main..main` returns empty
|
||||||
- [ ] Tag is pushed: `git ls-remote --tags origin vVERSION` returns the tag SHA
|
- [ ] Tag is pushed: `git ls-remote --tags origin vVERSION` returns the tag SHA
|
||||||
|
|
||||||
After GitHub Release:
|
After GitHub Release:
|
||||||
|
|
||||||
- [ ] Release is published (NOT draft): `gh release view "vVERSION"` output doesn't contain "(draft)"
|
- [ ] Release is published (NOT draft): `gh release view "vVERSION"` output doesn't contain "(draft)"
|
||||||
- [ ] Workflow is running: `gh run list --workflow=publish.yml --limit 1` shows "in_progress"
|
- [ ] Workflow is running: `gh run list --workflow=publish.yml --limit 1` shows "in_progress"
|
||||||
|
|
||||||
After workflow completes:
|
After workflow completes:
|
||||||
|
|
||||||
- [ ] Both jobs succeeded: Workflow shows green checkmarks
|
- [ ] Both jobs succeeded: Workflow shows green checkmarks
|
||||||
- [ ] SDK on npm: `npm view @bradygaster/squad-sdk version` returns correct version
|
- [ ] SDK on npm: `npm view @bradygaster/squad-sdk version` returns correct version
|
||||||
- [ ] CLI on npm: `npm view @bradygaster/squad-cli version` returns correct version
|
- [ ] CLI on npm: `npm view @bradygaster/squad-cli version` returns correct version
|
||||||
- [ ] `latest` tags correct: `npm dist-tag ls @bradygaster/squad-sdk` shows `latest: VERSION`
|
- [ ] `latest` tags correct: `npm dist-tag ls @bradygaster/squad-sdk` shows `latest: VERSION`
|
||||||
- [ ] Packages install: `npm install @bradygaster/squad-cli` succeeds
|
- [ ] Packages install: `npm install @bradygaster/squad-cli` succeeds
|
||||||
|
|
||||||
After dev sync:
|
After dev sync:
|
||||||
|
|
||||||
- [ ] dev branch has next preview version: `git show dev:package.json | grep version` shows next preview
|
- [ ] dev branch has next preview version: `git show dev:package.json | grep version` shows next preview
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Post-Mortem Reference
|
## Post-Mortem Reference
|
||||||
|
|
||||||
This skill was created after the v0.8.22 release disaster. Full retrospective: `.squad/decisions/inbox/keaton-v0822-retrospective.md`
|
This skill was created after the v0.8.22 release disaster. Full retrospective: `.squad/decisions/inbox/keaton-v0822-retrospective.md`
|
||||||
|
|
||||||
**Key learnings:**
|
**Key learnings:**
|
||||||
1. No release without a runbook = improvisation = disaster
|
1. No release without a runbook = improvisation = disaster
|
||||||
2. Semver validation is mandatory — 4-part versions break npm
|
2. Semver validation is mandatory — 4-part versions break npm
|
||||||
3. NPM_TOKEN type matters — User tokens with 2FA fail in CI
|
3. NPM_TOKEN type matters — User tokens with 2FA fail in CI
|
||||||
4. Draft releases are a footgun — they don't trigger automation
|
4. Draft releases are a footgun — they don't trigger automation
|
||||||
5. Retry logic is essential — npm propagation takes time
|
5. Retry logic is essential — npm propagation takes time
|
||||||
|
|
||||||
**Never again.**
|
**Never again.**
|
||||||
|
|||||||
@@ -1,92 +1,92 @@
|
|||||||
---
|
---
|
||||||
name: "reskill"
|
name: "reskill"
|
||||||
description: "Team-wide charter and history optimization through skill extraction"
|
description: "Team-wide charter and history optimization through skill extraction"
|
||||||
domain: "team-optimization"
|
domain: "team-optimization"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "manual — Brady directive to reduce per-agent context overhead"
|
source: "manual — Brady directive to reduce per-agent context overhead"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
When the coordinator hears "team, reskill" (or similar: "optimize context", "slim down charters"), trigger a team-wide optimization pass. The goal: reduce per-agent context consumption by extracting shared patterns from charters and histories into reusable skills.
|
When the coordinator hears "team, reskill" (or similar: "optimize context", "slim down charters"), trigger a team-wide optimization pass. The goal: reduce per-agent context consumption by extracting shared patterns from charters and histories into reusable skills.
|
||||||
|
|
||||||
This is a periodic maintenance activity. Run whenever charter/history bloat is suspected.
|
This is a periodic maintenance activity. Run whenever charter/history bloat is suspected.
|
||||||
|
|
||||||
## Process
|
## Process
|
||||||
|
|
||||||
### Step 1: Audit
|
### Step 1: Audit
|
||||||
Read all agent charters and histories. Measure byte sizes. Identify:
|
Read all agent charters and histories. Measure byte sizes. Identify:
|
||||||
|
|
||||||
- **Boilerplate** — sections repeated across ≥3 charters with <10% variation (collaboration, model, boundaries template)
|
- **Boilerplate** — sections repeated across ≥3 charters with <10% variation (collaboration, model, boundaries template)
|
||||||
- **Shared knowledge** — domain knowledge duplicated in 2+ charters (incident postmortems, technical patterns)
|
- **Shared knowledge** — domain knowledge duplicated in 2+ charters (incident postmortems, technical patterns)
|
||||||
- **Mature learnings** — history entries appearing 3+ times across agents that should be promoted to skills
|
- **Mature learnings** — history entries appearing 3+ times across agents that should be promoted to skills
|
||||||
|
|
||||||
### Step 2: Extract
|
### Step 2: Extract
|
||||||
For each identified pattern:
|
For each identified pattern:
|
||||||
1. Create or update a skill at `.squad/skills/{skill-name}/SKILL.md`
|
1. Create or update a skill at `.squad/skills/{skill-name}/SKILL.md`
|
||||||
2. Follow the skill template format (frontmatter + Context + Patterns + Examples + Anti-Patterns)
|
2. Follow the skill template format (frontmatter + Context + Patterns + Examples + Anti-Patterns)
|
||||||
3. Set confidence: low (first observation), medium (2+ agents), high (team-wide)
|
3. Set confidence: low (first observation), medium (2+ agents), high (team-wide)
|
||||||
|
|
||||||
### Step 3: Trim
|
### Step 3: Trim
|
||||||
**Charters** — target ≤1.5KB per agent:
|
**Charters** — target ≤1.5KB per agent:
|
||||||
- Remove Collaboration section entirely (spawn prompt + agent-collaboration skill covers it)
|
- Remove Collaboration section entirely (spawn prompt + agent-collaboration skill covers it)
|
||||||
- Remove Voice section (tagline blockquote at top of charter already captures it)
|
- Remove Voice section (tagline blockquote at top of charter already captures it)
|
||||||
- Trim Model section to single line: `Preferred: {model}`
|
- Trim Model section to single line: `Preferred: {model}`
|
||||||
- Remove "When I'm unsure" boilerplate from Boundaries
|
- Remove "When I'm unsure" boilerplate from Boundaries
|
||||||
- Remove domain knowledge now covered by a skill — add skill reference comment if helpful
|
- Remove domain knowledge now covered by a skill — add skill reference comment if helpful
|
||||||
- Keep: Identity, What I Own, unique How I Work patterns, Boundaries (domain list only)
|
- Keep: Identity, What I Own, unique How I Work patterns, Boundaries (domain list only)
|
||||||
|
|
||||||
**Histories** — target ≤8KB per agent:
|
**Histories** — target ≤8KB per agent:
|
||||||
- Apply history-hygiene skill to any history >12KB
|
- Apply history-hygiene skill to any history >12KB
|
||||||
- Promote recurring patterns (3+ occurrences across agents) to skills
|
- Promote recurring patterns (3+ occurrences across agents) to skills
|
||||||
- Summarize old entries into `## Core Context` section
|
- Summarize old entries into `## Core Context` section
|
||||||
- Remove session-specific metadata (dates, branch names, requester names)
|
- Remove session-specific metadata (dates, branch names, requester names)
|
||||||
|
|
||||||
### Step 4: Report
|
### Step 4: Report
|
||||||
Output a savings table:
|
Output a savings table:
|
||||||
|
|
||||||
| Agent | Charter Before | Charter After | History Before | History After | Saved |
|
| Agent | Charter Before | Charter After | History Before | History After | Saved |
|
||||||
|-------|---------------|---------------|----------------|---------------|-------|
|
|-------|---------------|---------------|----------------|---------------|-------|
|
||||||
|
|
||||||
Include totals and percentage reduction.
|
Include totals and percentage reduction.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### Minimal Charter Template (target format after reskill)
|
### Minimal Charter Template (target format after reskill)
|
||||||
|
|
||||||
```
|
```
|
||||||
# {Name} — {Role}
|
# {Name} — {Role}
|
||||||
|
|
||||||
> {Tagline — one sentence capturing voice and philosophy}
|
> {Tagline — one sentence capturing voice and philosophy}
|
||||||
|
|
||||||
## Identity
|
## Identity
|
||||||
- **Name:** {Name}
|
- **Name:** {Name}
|
||||||
- **Role:** {Role}
|
- **Role:** {Role}
|
||||||
- **Expertise:** {comma-separated list}
|
- **Expertise:** {comma-separated list}
|
||||||
|
|
||||||
## What I Own
|
## What I Own
|
||||||
- {bullet list of owned artifacts/domains}
|
- {bullet list of owned artifacts/domains}
|
||||||
|
|
||||||
## How I Work
|
## How I Work
|
||||||
- {unique patterns and principles — NOT boilerplate}
|
- {unique patterns and principles — NOT boilerplate}
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
**I handle:** {domain list}
|
**I handle:** {domain list}
|
||||||
**I don't handle:** {explicit exclusions}
|
**I don't handle:** {explicit exclusions}
|
||||||
|
|
||||||
## Model
|
## Model
|
||||||
Preferred: {model}
|
Preferred: {model}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Skill Extraction Threshold
|
### Skill Extraction Threshold
|
||||||
- **1 charter** → leave in charter (unique to that agent)
|
- **1 charter** → leave in charter (unique to that agent)
|
||||||
- **2 charters** → consider extracting if >500 bytes of overlap
|
- **2 charters** → consider extracting if >500 bytes of overlap
|
||||||
- **3+ charters** → always extract to a shared skill
|
- **3+ charters** → always extract to a shared skill
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
- Don't delete unique per-agent identity or domain-specific knowledge
|
- Don't delete unique per-agent identity or domain-specific knowledge
|
||||||
- Don't create skills for content only one agent uses
|
- Don't create skills for content only one agent uses
|
||||||
- Don't merge unrelated patterns into a single mega-skill
|
- Don't merge unrelated patterns into a single mega-skill
|
||||||
- Don't remove Model preference line (coordinator needs it for model selection)
|
- Don't remove Model preference line (coordinator needs it for model selection)
|
||||||
- Don't touch `.squad/decisions.md` during reskill
|
- Don't touch `.squad/decisions.md` during reskill
|
||||||
- Don't remove the tagline blockquote — it's the charter's soul in one line
|
- Don't remove the tagline blockquote — it's the charter's soul in one line
|
||||||
|
|||||||
@@ -1,79 +1,79 @@
|
|||||||
---
|
---
|
||||||
name: "reviewer-protocol"
|
name: "reviewer-protocol"
|
||||||
description: "Reviewer rejection workflow and strict lockout semantics"
|
description: "Reviewer rejection workflow and strict lockout semantics"
|
||||||
domain: "orchestration"
|
domain: "orchestration"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "extracted"
|
source: "extracted"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
When a team member has a **Reviewer** role (e.g., Tester, Code Reviewer, Lead), they may approve or reject work from other agents. On rejection, the coordinator enforces strict lockout rules to ensure the original author does NOT self-revise. This prevents defensive feedback loops and ensures independent review.
|
When a team member has a **Reviewer** role (e.g., Tester, Code Reviewer, Lead), they may approve or reject work from other agents. On rejection, the coordinator enforces strict lockout rules to ensure the original author does NOT self-revise. This prevents defensive feedback loops and ensures independent review.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### Reviewer Rejection Protocol
|
### Reviewer Rejection Protocol
|
||||||
|
|
||||||
When a team member has a **Reviewer** role:
|
When a team member has a **Reviewer** role:
|
||||||
|
|
||||||
- Reviewers may **approve** or **reject** work from other agents.
|
- Reviewers may **approve** or **reject** work from other agents.
|
||||||
- On **rejection**, the Reviewer may choose ONE of:
|
- On **rejection**, the Reviewer may choose ONE of:
|
||||||
1. **Reassign:** Require a *different* agent to do the revision (not the original author).
|
1. **Reassign:** Require a *different* agent to do the revision (not the original author).
|
||||||
2. **Escalate:** Require a *new* agent be spawned with specific expertise.
|
2. **Escalate:** Require a *new* agent be spawned with specific expertise.
|
||||||
- The Coordinator MUST enforce this. If the Reviewer says "someone else should fix this," the original agent does NOT get to self-revise.
|
- The Coordinator MUST enforce this. If the Reviewer says "someone else should fix this," the original agent does NOT get to self-revise.
|
||||||
- If the Reviewer approves, work proceeds normally.
|
- If the Reviewer approves, work proceeds normally.
|
||||||
|
|
||||||
### Strict Lockout Semantics
|
### Strict Lockout Semantics
|
||||||
|
|
||||||
When an artifact is **rejected** by a Reviewer:
|
When an artifact is **rejected** by a Reviewer:
|
||||||
|
|
||||||
1. **The original author is locked out.** They may NOT produce the next version of that artifact. No exceptions.
|
1. **The original author is locked out.** They may NOT produce the next version of that artifact. No exceptions.
|
||||||
2. **A different agent MUST own the revision.** The Coordinator selects the revision author based on the Reviewer's recommendation (reassign or escalate).
|
2. **A different agent MUST own the revision.** The Coordinator selects the revision author based on the Reviewer's recommendation (reassign or escalate).
|
||||||
3. **The Coordinator enforces this mechanically.** Before spawning a revision agent, the Coordinator MUST verify that the selected agent is NOT the original author. If the Reviewer names the original author as the fix agent, the Coordinator MUST refuse and ask the Reviewer to name a different agent.
|
3. **The Coordinator enforces this mechanically.** Before spawning a revision agent, the Coordinator MUST verify that the selected agent is NOT the original author. If the Reviewer names the original author as the fix agent, the Coordinator MUST refuse and ask the Reviewer to name a different agent.
|
||||||
4. **The locked-out author may NOT contribute to the revision** in any form — not as a co-author, advisor, or pair. The revision must be independently produced.
|
4. **The locked-out author may NOT contribute to the revision** in any form — not as a co-author, advisor, or pair. The revision must be independently produced.
|
||||||
5. **Lockout scope:** The lockout applies to the specific artifact that was rejected. The original author may still work on other unrelated artifacts.
|
5. **Lockout scope:** The lockout applies to the specific artifact that was rejected. The original author may still work on other unrelated artifacts.
|
||||||
6. **Lockout duration:** The lockout persists for that revision cycle. If the revision is also rejected, the same rule applies again — the revision author is now also locked out, and a third agent must revise.
|
6. **Lockout duration:** The lockout persists for that revision cycle. If the revision is also rejected, the same rule applies again — the revision author is now also locked out, and a third agent must revise.
|
||||||
7. **Deadlock handling:** If all eligible agents have been locked out of an artifact, the Coordinator MUST escalate to the user rather than re-admitting a locked-out author.
|
7. **Deadlock handling:** If all eligible agents have been locked out of an artifact, the Coordinator MUST escalate to the user rather than re-admitting a locked-out author.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
**Example 1: Reassign after rejection**
|
**Example 1: Reassign after rejection**
|
||||||
1. Fenster writes authentication module
|
1. Fenster writes authentication module
|
||||||
2. Hockney (Tester) reviews → rejects: "Error handling is missing. Verbal should fix this."
|
2. Hockney (Tester) reviews → rejects: "Error handling is missing. Verbal should fix this."
|
||||||
3. Coordinator: Fenster is now locked out of this artifact
|
3. Coordinator: Fenster is now locked out of this artifact
|
||||||
4. Coordinator spawns Verbal to revise the authentication module
|
4. Coordinator spawns Verbal to revise the authentication module
|
||||||
5. Verbal produces v2
|
5. Verbal produces v2
|
||||||
6. Hockney reviews v2 → approves
|
6. Hockney reviews v2 → approves
|
||||||
7. Lockout clears for next artifact
|
7. Lockout clears for next artifact
|
||||||
|
|
||||||
**Example 2: Escalate for expertise**
|
**Example 2: Escalate for expertise**
|
||||||
1. Edie writes TypeScript config
|
1. Edie writes TypeScript config
|
||||||
2. Keaton (Lead) reviews → rejects: "Need someone with deeper TS knowledge. Escalate."
|
2. Keaton (Lead) reviews → rejects: "Need someone with deeper TS knowledge. Escalate."
|
||||||
3. Coordinator: Edie is now locked out
|
3. Coordinator: Edie is now locked out
|
||||||
4. Coordinator spawns new agent (or existing TS expert) to revise
|
4. Coordinator spawns new agent (or existing TS expert) to revise
|
||||||
5. New agent produces v2
|
5. New agent produces v2
|
||||||
6. Keaton reviews v2
|
6. Keaton reviews v2
|
||||||
|
|
||||||
**Example 3: Deadlock handling**
|
**Example 3: Deadlock handling**
|
||||||
1. Fenster writes module → rejected
|
1. Fenster writes module → rejected
|
||||||
2. Verbal revises → rejected
|
2. Verbal revises → rejected
|
||||||
3. Hockney revises → rejected
|
3. Hockney revises → rejected
|
||||||
4. All 3 eligible agents are now locked out
|
4. All 3 eligible agents are now locked out
|
||||||
5. Coordinator: "All eligible agents have been locked out. Escalating to user: [artifact details]"
|
5. Coordinator: "All eligible agents have been locked out. Escalating to user: [artifact details]"
|
||||||
|
|
||||||
**Example 4: Reviewer accidentally names original author**
|
**Example 4: Reviewer accidentally names original author**
|
||||||
1. Fenster writes module → rejected
|
1. Fenster writes module → rejected
|
||||||
2. Hockney says: "Fenster should fix the error handling"
|
2. Hockney says: "Fenster should fix the error handling"
|
||||||
3. Coordinator: "Fenster is locked out as the original author. Please name a different agent."
|
3. Coordinator: "Fenster is locked out as the original author. Please name a different agent."
|
||||||
4. Hockney: "Verbal, then"
|
4. Hockney: "Verbal, then"
|
||||||
5. Coordinator spawns Verbal
|
5. Coordinator spawns Verbal
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- ❌ Allowing the original author to self-revise after rejection
|
- ❌ Allowing the original author to self-revise after rejection
|
||||||
- ❌ Treating the locked-out author as an "advisor" or "co-author" on the revision
|
- ❌ Treating the locked-out author as an "advisor" or "co-author" on the revision
|
||||||
- ❌ Re-admitting a locked-out author when deadlock occurs (must escalate to user)
|
- ❌ Re-admitting a locked-out author when deadlock occurs (must escalate to user)
|
||||||
- ❌ Applying lockout across unrelated artifacts (scope is per-artifact)
|
- ❌ Applying lockout across unrelated artifacts (scope is per-artifact)
|
||||||
- ❌ Accepting the Reviewer's assignment when they name the original author (must refuse and ask for a different agent)
|
- ❌ Accepting the Reviewer's assignment when they name the original author (must refuse and ask for a different agent)
|
||||||
- ❌ Clearing lockout before the revision is approved (lockout persists through revision cycle)
|
- ❌ Clearing lockout before the revision is approved (lockout persists through revision cycle)
|
||||||
- ❌ Skipping verification that the revision agent is not the original author
|
- ❌ Skipping verification that the revision agent is not the original author
|
||||||
|
|||||||
@@ -1,200 +1,200 @@
|
|||||||
---
|
---
|
||||||
name: secret-handling
|
name: secret-handling
|
||||||
description: Never read .env files or write secrets to .squad/ committed files
|
description: Never read .env files or write secrets to .squad/ committed files
|
||||||
domain: security, file-operations, team-collaboration
|
domain: security, file-operations, team-collaboration
|
||||||
confidence: high
|
confidence: high
|
||||||
source: earned (issue #267 — credential leak incident)
|
source: earned (issue #267 — credential leak incident)
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Spawned agents have read access to the entire repository, including `.env` files containing live credentials. If an agent reads secrets and writes them to `.squad/` files (decisions, logs, history), Scribe auto-commits them to git, exposing them in remote history. This skill codifies absolute prohibitions and safe alternatives.
|
Spawned agents have read access to the entire repository, including `.env` files containing live credentials. If an agent reads secrets and writes them to `.squad/` files (decisions, logs, history), Scribe auto-commits them to git, exposing them in remote history. This skill codifies absolute prohibitions and safe alternatives.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### Prohibited File Reads
|
### Prohibited File Reads
|
||||||
|
|
||||||
**NEVER read these files:**
|
**NEVER read these files:**
|
||||||
- `.env` (production secrets)
|
- `.env` (production secrets)
|
||||||
- `.env.local` (local dev secrets)
|
- `.env.local` (local dev secrets)
|
||||||
- `.env.production` (production environment)
|
- `.env.production` (production environment)
|
||||||
- `.env.development` (development environment)
|
- `.env.development` (development environment)
|
||||||
- `.env.staging` (staging environment)
|
- `.env.staging` (staging environment)
|
||||||
- `.env.test` (test environment with real credentials)
|
- `.env.test` (test environment with real credentials)
|
||||||
- Any file matching `.env.*` UNLESS explicitly allowed (see below)
|
- Any file matching `.env.*` UNLESS explicitly allowed (see below)
|
||||||
|
|
||||||
**Allowed alternatives:**
|
**Allowed alternatives:**
|
||||||
- `.env.example` (safe — contains placeholder values, no real secrets)
|
- `.env.example` (safe — contains placeholder values, no real secrets)
|
||||||
- `.env.sample` (safe — documentation template)
|
- `.env.sample` (safe — documentation template)
|
||||||
- `.env.template` (safe — schema/structure reference)
|
- `.env.template` (safe — schema/structure reference)
|
||||||
|
|
||||||
**If you need config info:**
|
**If you need config info:**
|
||||||
1. **Ask the user directly** — "What's the database connection string?"
|
1. **Ask the user directly** — "What's the database connection string?"
|
||||||
2. **Read `.env.example`** — shows structure without exposing secrets
|
2. **Read `.env.example`** — shows structure without exposing secrets
|
||||||
3. **Read documentation** — check `README.md`, `docs/`, config guides
|
3. **Read documentation** — check `README.md`, `docs/`, config guides
|
||||||
|
|
||||||
**NEVER assume you can "just peek at .env to understand the schema."** Use `.env.example` or ask.
|
**NEVER assume you can "just peek at .env to understand the schema."** Use `.env.example` or ask.
|
||||||
|
|
||||||
### Prohibited Output Patterns
|
### Prohibited Output Patterns
|
||||||
|
|
||||||
**NEVER write these to `.squad/` files:**
|
**NEVER write these to `.squad/` files:**
|
||||||
|
|
||||||
| Pattern Type | Examples | Regex Pattern (for scanning) |
|
| Pattern Type | Examples | Regex Pattern (for scanning) |
|
||||||
|--------------|----------|-------------------------------|
|
|--------------|----------|-------------------------------|
|
||||||
| API Keys | `OPENAI_API_KEY=sk-proj-...`, `GITHUB_TOKEN=ghp_...` | `[A-Z_]+(?:KEY|TOKEN|SECRET)=[^\s]+` |
|
| API Keys | `OPENAI_API_KEY=sk-proj-...`, `GITHUB_TOKEN=ghp_...` | `[A-Z_]+(?:KEY|TOKEN|SECRET)=[^\s]+` |
|
||||||
| Passwords | `DB_PASSWORD=super_secret_123`, `password: "..."` | `(?:PASSWORD|PASS|PWD)[:=]\s*["']?[^\s"']+` |
|
| Passwords | `DB_PASSWORD=super_secret_123`, `password: "..."` | `(?:PASSWORD|PASS|PWD)[:=]\s*["']?[^\s"']+` |
|
||||||
| Connection Strings | `postgres://user:pass@host:5432/db`, `Server=...;Password=...` | `(?:postgres|mysql|mongodb)://[^@]+@|(?:Server|Host)=.*(?:Password|Pwd)=` |
|
| Connection Strings | `postgres://user:pass@host:5432/db`, `Server=...;Password=...` | `(?:postgres|mysql|mongodb)://[^@]+@|(?:Server|Host)=.*(?:Password|Pwd)=` |
|
||||||
| JWT Tokens | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...` | `eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+` |
|
| JWT Tokens | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...` | `eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+` |
|
||||||
| Private Keys | `-----BEGIN PRIVATE KEY-----`, `-----BEGIN RSA PRIVATE KEY-----` | `-----BEGIN [A-Z ]+PRIVATE KEY-----` |
|
| Private Keys | `-----BEGIN PRIVATE KEY-----`, `-----BEGIN RSA PRIVATE KEY-----` | `-----BEGIN [A-Z ]+PRIVATE KEY-----` |
|
||||||
| AWS Credentials | `AKIA...`, `aws_secret_access_key=...` | `AKIA[0-9A-Z]{16}|aws_secret_access_key=[^\s]+` |
|
| AWS Credentials | `AKIA...`, `aws_secret_access_key=...` | `AKIA[0-9A-Z]{16}|aws_secret_access_key=[^\s]+` |
|
||||||
| Email Addresses | `user@example.com` (PII violation per team decision) | `[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}` |
|
| Email Addresses | `user@example.com` (PII violation per team decision) | `[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}` |
|
||||||
|
|
||||||
**What to write instead:**
|
**What to write instead:**
|
||||||
- Placeholder values: `DATABASE_URL=<set in .env>`
|
- Placeholder values: `DATABASE_URL=<set in .env>`
|
||||||
- Redacted references: `API key configured (see .env.example)`
|
- Redacted references: `API key configured (see .env.example)`
|
||||||
- Architecture notes: "App uses JWT auth — token stored in session"
|
- Architecture notes: "App uses JWT auth — token stored in session"
|
||||||
- Schema documentation: "Requires OPENAI_API_KEY, GITHUB_TOKEN (see .env.example for format)"
|
- Schema documentation: "Requires OPENAI_API_KEY, GITHUB_TOKEN (see .env.example for format)"
|
||||||
|
|
||||||
### Scribe Pre-Commit Validation
|
### Scribe Pre-Commit Validation
|
||||||
|
|
||||||
**Before committing `.squad/` changes, Scribe MUST:**
|
**Before committing `.squad/` changes, Scribe MUST:**
|
||||||
|
|
||||||
1. **Scan all staged files** for secret patterns (use regex table above)
|
1. **Scan all staged files** for secret patterns (use regex table above)
|
||||||
2. **Check for prohibited file names** (don't commit `.env` even if manually staged)
|
2. **Check for prohibited file names** (don't commit `.env` even if manually staged)
|
||||||
3. **If secrets detected:**
|
3. **If secrets detected:**
|
||||||
- STOP the commit (do NOT proceed)
|
- STOP the commit (do NOT proceed)
|
||||||
- Remove the file from staging: `git reset HEAD <file>`
|
- Remove the file from staging: `git reset HEAD <file>`
|
||||||
- Report to user:
|
- Report to user:
|
||||||
```
|
```
|
||||||
🚨 SECRET DETECTED — commit blocked
|
🚨 SECRET DETECTED — commit blocked
|
||||||
|
|
||||||
File: .squad/decisions/inbox/river-db-config.md
|
File: .squad/decisions/inbox/river-db-config.md
|
||||||
Pattern: DATABASE_URL=postgres://user:password@localhost:5432/prod
|
Pattern: DATABASE_URL=postgres://user:password@localhost:5432/prod
|
||||||
|
|
||||||
This file contains credentials and MUST NOT be committed.
|
This file contains credentials and MUST NOT be committed.
|
||||||
Please remove the secret, replace with placeholder, and try again.
|
Please remove the secret, replace with placeholder, and try again.
|
||||||
```
|
```
|
||||||
- Exit with error (never silently skip)
|
- Exit with error (never silently skip)
|
||||||
|
|
||||||
4. **If no secrets detected:**
|
4. **If no secrets detected:**
|
||||||
- Proceed with commit as normal
|
- Proceed with commit as normal
|
||||||
|
|
||||||
**Implementation note for Scribe:**
|
**Implementation note for Scribe:**
|
||||||
- Run validation AFTER staging files, BEFORE calling `git commit`
|
- Run validation AFTER staging files, BEFORE calling `git commit`
|
||||||
- Use PowerShell `Select-String` or `git diff --cached` to scan staged content
|
- Use PowerShell `Select-String` or `git diff --cached` to scan staged content
|
||||||
- Fail loud — secret leaks are unacceptable, blocking the commit is correct behavior
|
- Fail loud — secret leaks are unacceptable, blocking the commit is correct behavior
|
||||||
|
|
||||||
### Remediation — If a Secret Was Already Committed
|
### Remediation — If a Secret Was Already Committed
|
||||||
|
|
||||||
**If you discover a secret in git history:**
|
**If you discover a secret in git history:**
|
||||||
|
|
||||||
1. **STOP immediately** — do not make more commits
|
1. **STOP immediately** — do not make more commits
|
||||||
2. **Alert the user:**
|
2. **Alert the user:**
|
||||||
```
|
```
|
||||||
🚨 CREDENTIAL LEAK DETECTED
|
🚨 CREDENTIAL LEAK DETECTED
|
||||||
|
|
||||||
A secret was found in git history:
|
A secret was found in git history:
|
||||||
Commit: abc1234
|
Commit: abc1234
|
||||||
File: .squad/decisions/inbox/agent-config.md
|
File: .squad/decisions/inbox/agent-config.md
|
||||||
Pattern: API_KEY=sk-proj-...
|
Pattern: API_KEY=sk-proj-...
|
||||||
|
|
||||||
This requires immediate remediation:
|
This requires immediate remediation:
|
||||||
1. Revoke the exposed credential (regenerate API key, rotate password)
|
1. Revoke the exposed credential (regenerate API key, rotate password)
|
||||||
2. Remove from git history (git filter-repo or BFG)
|
2. Remove from git history (git filter-repo or BFG)
|
||||||
3. Force-push the cleaned history
|
3. Force-push the cleaned history
|
||||||
|
|
||||||
Do NOT proceed with new work until this is resolved.
|
Do NOT proceed with new work until this is resolved.
|
||||||
```
|
```
|
||||||
3. **Do NOT attempt to fix it yourself** — secret removal requires specialized tools
|
3. **Do NOT attempt to fix it yourself** — secret removal requires specialized tools
|
||||||
4. **Wait for user confirmation** before resuming work
|
4. **Wait for user confirmation** before resuming work
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### ✓ Correct: Reading Config Schema
|
### ✓ Correct: Reading Config Schema
|
||||||
|
|
||||||
**Agent needs to know what environment variables are required:**
|
**Agent needs to know what environment variables are required:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Agent: "What environment variables does this app need?"
|
Agent: "What environment variables does this app need?"
|
||||||
→ Reads `.env.example`:
|
→ Reads `.env.example`:
|
||||||
OPENAI_API_KEY=sk-...
|
OPENAI_API_KEY=sk-...
|
||||||
DATABASE_URL=postgres://user:pass@localhost:5432/db
|
DATABASE_URL=postgres://user:pass@localhost:5432/db
|
||||||
REDIS_URL=redis://localhost:6379
|
REDIS_URL=redis://localhost:6379
|
||||||
|
|
||||||
→ Writes to .squad/decisions/inbox/river-env-setup.md:
|
→ Writes to .squad/decisions/inbox/river-env-setup.md:
|
||||||
"App requires three environment variables:
|
"App requires three environment variables:
|
||||||
- OPENAI_API_KEY (OpenAI API key, format: sk-...)
|
- OPENAI_API_KEY (OpenAI API key, format: sk-...)
|
||||||
- DATABASE_URL (Postgres connection string)
|
- DATABASE_URL (Postgres connection string)
|
||||||
- REDIS_URL (Redis connection string)
|
- REDIS_URL (Redis connection string)
|
||||||
See .env.example for full schema."
|
See .env.example for full schema."
|
||||||
```
|
```
|
||||||
|
|
||||||
### ✗ Incorrect: Reading Live Credentials
|
### ✗ Incorrect: Reading Live Credentials
|
||||||
|
|
||||||
**Agent needs to know database schema:**
|
**Agent needs to know database schema:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Agent: (reads .env)
|
Agent: (reads .env)
|
||||||
DATABASE_URL=postgres://admin:super_secret_pw@prod.example.com:5432/appdb
|
DATABASE_URL=postgres://admin:super_secret_pw@prod.example.com:5432/appdb
|
||||||
|
|
||||||
→ Writes to .squad/decisions/inbox/river-db-schema.md:
|
→ Writes to .squad/decisions/inbox/river-db-schema.md:
|
||||||
"Database connection: postgres://admin:super_secret_pw@prod.example.com:5432/appdb"
|
"Database connection: postgres://admin:super_secret_pw@prod.example.com:5432/appdb"
|
||||||
|
|
||||||
🚨 VIOLATION: Live credential written to committed file
|
🚨 VIOLATION: Live credential written to committed file
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct approach:**
|
**Correct approach:**
|
||||||
```
|
```
|
||||||
Agent: (reads .env.example OR asks user)
|
Agent: (reads .env.example OR asks user)
|
||||||
User: "It's a Postgres database, schema is in migrations/"
|
User: "It's a Postgres database, schema is in migrations/"
|
||||||
|
|
||||||
→ Writes to .squad/decisions/inbox/river-db-schema.md:
|
→ Writes to .squad/decisions/inbox/river-db-schema.md:
|
||||||
"Database: Postgres (connection configured in .env). Schema defined in db/migrations/."
|
"Database: Postgres (connection configured in .env). Schema defined in db/migrations/."
|
||||||
```
|
```
|
||||||
|
|
||||||
### ✓ Correct: Scribe Pre-Commit Validation
|
### ✓ Correct: Scribe Pre-Commit Validation
|
||||||
|
|
||||||
**Scribe is about to commit:**
|
**Scribe is about to commit:**
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Stage files
|
# Stage files
|
||||||
git add .squad/
|
git add .squad/
|
||||||
|
|
||||||
# Scan staged content for secrets
|
# Scan staged content for secrets
|
||||||
$stagedContent = git diff --cached
|
$stagedContent = git diff --cached
|
||||||
$secretPatterns = @(
|
$secretPatterns = @(
|
||||||
'[A-Z_]+(?:KEY|TOKEN|SECRET)=[^\s]+',
|
'[A-Z_]+(?:KEY|TOKEN|SECRET)=[^\s]+',
|
||||||
'(?:PASSWORD|PASS|PWD)[:=]\s*["'']?[^\s"'']+',
|
'(?:PASSWORD|PASS|PWD)[:=]\s*["'']?[^\s"'']+',
|
||||||
'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'
|
'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'
|
||||||
)
|
)
|
||||||
|
|
||||||
$detected = $false
|
$detected = $false
|
||||||
foreach ($pattern in $secretPatterns) {
|
foreach ($pattern in $secretPatterns) {
|
||||||
if ($stagedContent -match $pattern) {
|
if ($stagedContent -match $pattern) {
|
||||||
$detected = $true
|
$detected = $true
|
||||||
Write-Host "🚨 SECRET DETECTED: $($matches[0])"
|
Write-Host "🚨 SECRET DETECTED: $($matches[0])"
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($detected) {
|
if ($detected) {
|
||||||
# Remove from staging, report, exit
|
# Remove from staging, report, exit
|
||||||
git reset HEAD .squad/
|
git reset HEAD .squad/
|
||||||
Write-Error "Commit blocked — secret detected in staged files"
|
Write-Error "Commit blocked — secret detected in staged files"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Safe to commit
|
# Safe to commit
|
||||||
git commit -F $msgFile
|
git commit -F $msgFile
|
||||||
```
|
```
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- ❌ Reading `.env` "just to check the schema" — use `.env.example` instead
|
- ❌ Reading `.env` "just to check the schema" — use `.env.example` instead
|
||||||
- ❌ Writing "sanitized" connection strings that still contain credentials
|
- ❌ Writing "sanitized" connection strings that still contain credentials
|
||||||
- ❌ Assuming "it's just a dev environment" makes secrets safe to commit
|
- ❌ Assuming "it's just a dev environment" makes secrets safe to commit
|
||||||
- ❌ Committing first, scanning later — validation MUST happen before commit
|
- ❌ Committing first, scanning later — validation MUST happen before commit
|
||||||
- ❌ Silently skipping secret detection — fail loud, never silent
|
- ❌ Silently skipping secret detection — fail loud, never silent
|
||||||
- ❌ Trusting agents to "know better" — enforce at multiple layers (prompt, hook, architecture)
|
- ❌ Trusting agents to "know better" — enforce at multiple layers (prompt, hook, architecture)
|
||||||
- ❌ Writing secrets to "temporary" files in `.squad/` — Scribe commits ALL `.squad/` changes
|
- ❌ Writing secrets to "temporary" files in `.squad/` — Scribe commits ALL `.squad/` changes
|
||||||
- ❌ Extracting "just the host" from a connection string — still leaks infrastructure topology
|
- ❌ Extracting "just the host" from a connection string — still leaks infrastructure topology
|
||||||
|
|||||||
@@ -1,155 +1,155 @@
|
|||||||
---
|
---
|
||||||
name: "session-recovery"
|
name: "session-recovery"
|
||||||
description: "Find and resume interrupted Copilot CLI sessions using session_store queries"
|
description: "Find and resume interrupted Copilot CLI sessions using session_store queries"
|
||||||
domain: "workflow-recovery"
|
domain: "workflow-recovery"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "earned"
|
source: "earned"
|
||||||
tools:
|
tools:
|
||||||
- name: "sql"
|
- name: "sql"
|
||||||
description: "Query session_store database for past session history"
|
description: "Query session_store database for past session history"
|
||||||
when: "Always — session_store is the source of truth for session history"
|
when: "Always — session_store is the source of truth for session history"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Squad agents run in Copilot CLI sessions that can be interrupted — terminal crashes, network drops, machine restarts, or accidental window closes. When this happens, in-progress work may be left in a partially-completed state: branches with uncommitted changes, issues marked in-progress with no active agent, or checkpoints that were never finalized.
|
Squad agents run in Copilot CLI sessions that can be interrupted — terminal crashes, network drops, machine restarts, or accidental window closes. When this happens, in-progress work may be left in a partially-completed state: branches with uncommitted changes, issues marked in-progress with no active agent, or checkpoints that were never finalized.
|
||||||
|
|
||||||
Copilot CLI stores session history in a SQLite database called `session_store` (read-only, accessed via the `sql` tool with `database: "session_store"`). This skill teaches agents how to query that store to detect interrupted sessions and resume work.
|
Copilot CLI stores session history in a SQLite database called `session_store` (read-only, accessed via the `sql` tool with `database: "session_store"`). This skill teaches agents how to query that store to detect interrupted sessions and resume work.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### 1. Find Recent Sessions
|
### 1. Find Recent Sessions
|
||||||
|
|
||||||
Query the `sessions` table filtered by time window. Include the last checkpoint to understand where the session stopped:
|
Query the `sessions` table filtered by time window. Include the last checkpoint to understand where the session stopped:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT
|
SELECT
|
||||||
s.id,
|
s.id,
|
||||||
s.summary,
|
s.summary,
|
||||||
s.cwd,
|
s.cwd,
|
||||||
s.branch,
|
s.branch,
|
||||||
s.updated_at,
|
s.updated_at,
|
||||||
(SELECT title FROM checkpoints
|
(SELECT title FROM checkpoints
|
||||||
WHERE session_id = s.id
|
WHERE session_id = s.id
|
||||||
ORDER BY checkpoint_number DESC LIMIT 1) AS last_checkpoint
|
ORDER BY checkpoint_number DESC LIMIT 1) AS last_checkpoint
|
||||||
FROM sessions s
|
FROM sessions s
|
||||||
WHERE s.updated_at >= datetime('now', '-24 hours')
|
WHERE s.updated_at >= datetime('now', '-24 hours')
|
||||||
ORDER BY s.updated_at DESC;
|
ORDER BY s.updated_at DESC;
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Filter Out Automated Sessions
|
### 2. Filter Out Automated Sessions
|
||||||
|
|
||||||
Automated agents (monitors, keep-alive, heartbeat) create high-volume sessions that obscure human-initiated work. Exclude them:
|
Automated agents (monitors, keep-alive, heartbeat) create high-volume sessions that obscure human-initiated work. Exclude them:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT s.id, s.summary, s.cwd, s.updated_at,
|
SELECT s.id, s.summary, s.cwd, s.updated_at,
|
||||||
(SELECT title FROM checkpoints
|
(SELECT title FROM checkpoints
|
||||||
WHERE session_id = s.id
|
WHERE session_id = s.id
|
||||||
ORDER BY checkpoint_number DESC LIMIT 1) AS last_checkpoint
|
ORDER BY checkpoint_number DESC LIMIT 1) AS last_checkpoint
|
||||||
FROM sessions s
|
FROM sessions s
|
||||||
WHERE s.updated_at >= datetime('now', '-24 hours')
|
WHERE s.updated_at >= datetime('now', '-24 hours')
|
||||||
AND s.id NOT IN (
|
AND s.id NOT IN (
|
||||||
SELECT DISTINCT t.session_id FROM turns t
|
SELECT DISTINCT t.session_id FROM turns t
|
||||||
WHERE t.turn_index = 0
|
WHERE t.turn_index = 0
|
||||||
AND (LOWER(t.user_message) LIKE '%keep-alive%'
|
AND (LOWER(t.user_message) LIKE '%keep-alive%'
|
||||||
OR LOWER(t.user_message) LIKE '%heartbeat%')
|
OR LOWER(t.user_message) LIKE '%heartbeat%')
|
||||||
)
|
)
|
||||||
ORDER BY s.updated_at DESC;
|
ORDER BY s.updated_at DESC;
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Search by Topic (FTS5)
|
### 3. Search by Topic (FTS5)
|
||||||
|
|
||||||
Use the `search_index` FTS5 table for keyword search. Expand queries with synonyms since this is keyword-based, not semantic:
|
Use the `search_index` FTS5 table for keyword search. Expand queries with synonyms since this is keyword-based, not semantic:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT DISTINCT s.id, s.summary, s.cwd, s.updated_at
|
SELECT DISTINCT s.id, s.summary, s.cwd, s.updated_at
|
||||||
FROM search_index si
|
FROM search_index si
|
||||||
JOIN sessions s ON si.session_id = s.id
|
JOIN sessions s ON si.session_id = s.id
|
||||||
WHERE search_index MATCH 'auth OR login OR token OR JWT'
|
WHERE search_index MATCH 'auth OR login OR token OR JWT'
|
||||||
AND s.updated_at >= datetime('now', '-48 hours')
|
AND s.updated_at >= datetime('now', '-48 hours')
|
||||||
ORDER BY s.updated_at DESC
|
ORDER BY s.updated_at DESC
|
||||||
LIMIT 10;
|
LIMIT 10;
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Search by Working Directory
|
### 4. Search by Working Directory
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT s.id, s.summary, s.updated_at,
|
SELECT s.id, s.summary, s.updated_at,
|
||||||
(SELECT title FROM checkpoints
|
(SELECT title FROM checkpoints
|
||||||
WHERE session_id = s.id
|
WHERE session_id = s.id
|
||||||
ORDER BY checkpoint_number DESC LIMIT 1) AS last_checkpoint
|
ORDER BY checkpoint_number DESC LIMIT 1) AS last_checkpoint
|
||||||
FROM sessions s
|
FROM sessions s
|
||||||
WHERE s.cwd LIKE '%my-project%'
|
WHERE s.cwd LIKE '%my-project%'
|
||||||
AND s.updated_at >= datetime('now', '-48 hours')
|
AND s.updated_at >= datetime('now', '-48 hours')
|
||||||
ORDER BY s.updated_at DESC;
|
ORDER BY s.updated_at DESC;
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. Get Full Session Context Before Resuming
|
### 5. Get Full Session Context Before Resuming
|
||||||
|
|
||||||
Before resuming, inspect what the session was doing:
|
Before resuming, inspect what the session was doing:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- Conversation turns
|
-- Conversation turns
|
||||||
SELECT turn_index, substr(user_message, 1, 200) AS ask, timestamp
|
SELECT turn_index, substr(user_message, 1, 200) AS ask, timestamp
|
||||||
FROM turns WHERE session_id = 'SESSION_ID' ORDER BY turn_index;
|
FROM turns WHERE session_id = 'SESSION_ID' ORDER BY turn_index;
|
||||||
|
|
||||||
-- Checkpoint progress
|
-- Checkpoint progress
|
||||||
SELECT checkpoint_number, title, overview
|
SELECT checkpoint_number, title, overview
|
||||||
FROM checkpoints WHERE session_id = 'SESSION_ID' ORDER BY checkpoint_number;
|
FROM checkpoints WHERE session_id = 'SESSION_ID' ORDER BY checkpoint_number;
|
||||||
|
|
||||||
-- Files touched
|
-- Files touched
|
||||||
SELECT file_path, tool_name
|
SELECT file_path, tool_name
|
||||||
FROM session_files WHERE session_id = 'SESSION_ID';
|
FROM session_files WHERE session_id = 'SESSION_ID';
|
||||||
|
|
||||||
-- Linked PRs/issues/commits
|
-- Linked PRs/issues/commits
|
||||||
SELECT ref_type, ref_value
|
SELECT ref_type, ref_value
|
||||||
FROM session_refs WHERE session_id = 'SESSION_ID';
|
FROM session_refs WHERE session_id = 'SESSION_ID';
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. Detect Orphaned Issue Work
|
### 6. Detect Orphaned Issue Work
|
||||||
|
|
||||||
Find sessions that were working on issues but may not have completed:
|
Find sessions that were working on issues but may not have completed:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT DISTINCT s.id, s.branch, s.summary, s.updated_at,
|
SELECT DISTINCT s.id, s.branch, s.summary, s.updated_at,
|
||||||
sr.ref_type, sr.ref_value
|
sr.ref_type, sr.ref_value
|
||||||
FROM sessions s
|
FROM sessions s
|
||||||
JOIN session_refs sr ON s.id = sr.session_id
|
JOIN session_refs sr ON s.id = sr.session_id
|
||||||
WHERE sr.ref_type = 'issue'
|
WHERE sr.ref_type = 'issue'
|
||||||
AND s.updated_at >= datetime('now', '-48 hours')
|
AND s.updated_at >= datetime('now', '-48 hours')
|
||||||
ORDER BY s.updated_at DESC;
|
ORDER BY s.updated_at DESC;
|
||||||
```
|
```
|
||||||
|
|
||||||
Cross-reference with `gh issue list --label "status:in-progress"` to find issues that are marked in-progress but have no active session.
|
Cross-reference with `gh issue list --label "status:in-progress"` to find issues that are marked in-progress but have no active session.
|
||||||
|
|
||||||
### 7. Resume a Session
|
### 7. Resume a Session
|
||||||
|
|
||||||
Once you have the session ID:
|
Once you have the session ID:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Resume directly
|
# Resume directly
|
||||||
copilot --resume SESSION_ID
|
copilot --resume SESSION_ID
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
**Recovering from a crash during PR creation:**
|
**Recovering from a crash during PR creation:**
|
||||||
1. Query recent sessions filtered by branch name
|
1. Query recent sessions filtered by branch name
|
||||||
2. Find the session that was working on the PR
|
2. Find the session that was working on the PR
|
||||||
3. Check its last checkpoint — was the code committed? Was the PR created?
|
3. Check its last checkpoint — was the code committed? Was the PR created?
|
||||||
4. Resume or manually complete the remaining steps
|
4. Resume or manually complete the remaining steps
|
||||||
|
|
||||||
**Finding yesterday's work on a feature:**
|
**Finding yesterday's work on a feature:**
|
||||||
1. Use FTS5 search with feature keywords
|
1. Use FTS5 search with feature keywords
|
||||||
2. Filter to the relevant working directory
|
2. Filter to the relevant working directory
|
||||||
3. Review checkpoint progress to see how far the session got
|
3. Review checkpoint progress to see how far the session got
|
||||||
4. Resume if work remains, or start fresh with the context
|
4. Resume if work remains, or start fresh with the context
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- ❌ Searching by partial session IDs — always use full UUIDs
|
- ❌ Searching by partial session IDs — always use full UUIDs
|
||||||
- ❌ Resuming sessions that completed successfully — they have no pending work
|
- ❌ Resuming sessions that completed successfully — they have no pending work
|
||||||
- ❌ Using `MATCH` with special characters without escaping — wrap paths in double quotes
|
- ❌ Using `MATCH` with special characters without escaping — wrap paths in double quotes
|
||||||
- ❌ Skipping the automated-session filter — high-volume automated sessions will flood results
|
- ❌ Skipping the automated-session filter — high-volume automated sessions will flood results
|
||||||
- ❌ Assuming FTS5 is semantic search — it's keyword-based; always expand queries with synonyms
|
- ❌ Assuming FTS5 is semantic search — it's keyword-based; always expand queries with synonyms
|
||||||
- ❌ Ignoring checkpoint data — checkpoints show exactly where the session stopped
|
- ❌ Ignoring checkpoint data — checkpoints show exactly where the session stopped
|
||||||
|
|||||||
@@ -1,69 +1,69 @@
|
|||||||
---
|
---
|
||||||
name: "squad-conventions"
|
name: "squad-conventions"
|
||||||
description: "Core conventions and patterns used in the Squad codebase"
|
description: "Core conventions and patterns used in the Squad codebase"
|
||||||
domain: "project-conventions"
|
domain: "project-conventions"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "manual"
|
source: "manual"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
These conventions apply to all work on the Squad CLI tool (`create-squad`). Squad is a zero-dependency Node.js package that adds AI agent teams to any project. Understanding these patterns is essential before modifying any Squad source code.
|
These conventions apply to all work on the Squad CLI tool (`create-squad`). Squad is a zero-dependency Node.js package that adds AI agent teams to any project. Understanding these patterns is essential before modifying any Squad source code.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
### Zero Dependencies
|
### Zero Dependencies
|
||||||
Squad has zero runtime dependencies. Everything uses Node.js built-ins (`fs`, `path`, `os`, `child_process`). Do not add packages to `dependencies` in `package.json`. This is a hard constraint, not a preference.
|
Squad has zero runtime dependencies. Everything uses Node.js built-ins (`fs`, `path`, `os`, `child_process`). Do not add packages to `dependencies` in `package.json`. This is a hard constraint, not a preference.
|
||||||
|
|
||||||
### Node.js Built-in Test Runner
|
### Node.js Built-in Test Runner
|
||||||
Tests use `node:test` and `node:assert/strict` — no test frameworks. Run with `npm test`. Test files live in `test/`. The test command is `node --test test/`.
|
Tests use `node:test` and `node:assert/strict` — no test frameworks. Run with `npm test`. Test files live in `test/`. The test command is `node --test test/`.
|
||||||
|
|
||||||
### Error Handling — `fatal()` Pattern
|
### Error Handling — `fatal()` Pattern
|
||||||
All user-facing errors use the `fatal(msg)` function which prints a red `✗` prefix and exits with code 1. Never throw unhandled exceptions or print raw stack traces. The global `uncaughtException` handler calls `fatal()` as a safety net.
|
All user-facing errors use the `fatal(msg)` function which prints a red `✗` prefix and exits with code 1. Never throw unhandled exceptions or print raw stack traces. The global `uncaughtException` handler calls `fatal()` as a safety net.
|
||||||
|
|
||||||
### ANSI Color Constants
|
### ANSI Color Constants
|
||||||
Colors are defined as constants at the top of `index.js`: `GREEN`, `RED`, `DIM`, `BOLD`, `RESET`. Use these constants — do not inline ANSI escape codes.
|
Colors are defined as constants at the top of `index.js`: `GREEN`, `RED`, `DIM`, `BOLD`, `RESET`. Use these constants — do not inline ANSI escape codes.
|
||||||
|
|
||||||
### File Structure
|
### File Structure
|
||||||
- `.squad/` — Team state (user-owned, never overwritten by upgrades)
|
- `.squad/` — Team state (user-owned, never overwritten by upgrades)
|
||||||
- `.squad/templates/` — Template files copied from `templates/` (Squad-owned, overwritten on upgrade)
|
- `.squad/templates/` — Template files copied from `templates/` (Squad-owned, overwritten on upgrade)
|
||||||
- `.github/agents/squad.agent.md` — Coordinator prompt (Squad-owned, overwritten on upgrade)
|
- `.github/agents/squad.agent.md` — Coordinator prompt (Squad-owned, overwritten on upgrade)
|
||||||
- `templates/` — Source templates shipped with the npm package
|
- `templates/` — Source templates shipped with the npm package
|
||||||
- `.squad/skills/` — Team skills in SKILL.md format (user-owned)
|
- `.squad/skills/` — Team skills in SKILL.md format (user-owned)
|
||||||
- `.squad/decisions/inbox/` — Drop-box for parallel decision writes
|
- `.squad/decisions/inbox/` — Drop-box for parallel decision writes
|
||||||
|
|
||||||
### Windows Compatibility
|
### Windows Compatibility
|
||||||
Always use `path.join()` for file paths — never hardcode `/` or `\` separators. Squad must work on Windows, macOS, and Linux. All tests must pass on all platforms.
|
Always use `path.join()` for file paths — never hardcode `/` or `\` separators. Squad must work on Windows, macOS, and Linux. All tests must pass on all platforms.
|
||||||
|
|
||||||
### Init Idempotency
|
### Init Idempotency
|
||||||
The init flow uses a skip-if-exists pattern: if a file or directory already exists, skip it and report "already exists." Never overwrite user state during init. The upgrade flow overwrites only Squad-owned files.
|
The init flow uses a skip-if-exists pattern: if a file or directory already exists, skip it and report "already exists." Never overwrite user state during init. The upgrade flow overwrites only Squad-owned files.
|
||||||
|
|
||||||
### Copy Pattern
|
### Copy Pattern
|
||||||
`copyRecursive(src, target)` handles both files and directories. It creates parent directories with `{ recursive: true }` and uses `fs.copyFileSync` for files.
|
`copyRecursive(src, target)` handles both files and directories. It creates parent directories with `{ recursive: true }` and uses `fs.copyFileSync` for files.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Error handling
|
// Error handling
|
||||||
function fatal(msg) {
|
function fatal(msg) {
|
||||||
console.error(`${RED}✗${RESET} ${msg}`);
|
console.error(`${RED}✗${RESET} ${msg}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// File path construction (Windows-safe)
|
// File path construction (Windows-safe)
|
||||||
const agentDest = path.join(dest, '.github', 'agents', 'squad.agent.md');
|
const agentDest = path.join(dest, '.github', 'agents', 'squad.agent.md');
|
||||||
|
|
||||||
// Skip-if-exists pattern
|
// Skip-if-exists pattern
|
||||||
if (!fs.existsSync(ceremoniesDest)) {
|
if (!fs.existsSync(ceremoniesDest)) {
|
||||||
fs.copyFileSync(ceremoniesSrc, ceremoniesDest);
|
fs.copyFileSync(ceremoniesSrc, ceremoniesDest);
|
||||||
console.log(`${GREEN}✓${RESET} .squad/ceremonies.md`);
|
console.log(`${GREEN}✓${RESET} .squad/ceremonies.md`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`${DIM}ceremonies.md already exists — skipping${RESET}`);
|
console.log(`${DIM}ceremonies.md already exists — skipping${RESET}`);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
- **Adding npm dependencies** — Squad is zero-dep. Use Node.js built-ins only.
|
- **Adding npm dependencies** — Squad is zero-dep. Use Node.js built-ins only.
|
||||||
- **Hardcoded path separators** — Never use `/` or `\` directly. Always `path.join()`.
|
- **Hardcoded path separators** — Never use `/` or `\` directly. Always `path.join()`.
|
||||||
- **Overwriting user state on init** — Init skips existing files. Only upgrade overwrites Squad-owned files.
|
- **Overwriting user state on init** — Init skips existing files. Only upgrade overwrites Squad-owned files.
|
||||||
- **Raw stack traces** — All errors go through `fatal()`. Users see clean messages, not stack traces.
|
- **Raw stack traces** — All errors go through `fatal()`. Users see clean messages, not stack traces.
|
||||||
- **Inline ANSI codes** — Use the color constants (`GREEN`, `RED`, `DIM`, `BOLD`, `RESET`).
|
- **Inline ANSI codes** — Use the color constants (`GREEN`, `RED`, `DIM`, `BOLD`, `RESET`).
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
---
|
---
|
||||||
name: "test-discipline"
|
name: "test-discipline"
|
||||||
description: "Update tests when changing APIs — no exceptions"
|
description: "Update tests when changing APIs — no exceptions"
|
||||||
domain: "quality"
|
domain: "quality"
|
||||||
confidence: "high"
|
confidence: "high"
|
||||||
source: "earned (Fenster/Hockney incident, test assertion sync violations)"
|
source: "earned (Fenster/Hockney incident, test assertion sync violations)"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
When APIs or public interfaces change, tests must be updated in the same commit. When test assertions reference file counts or expected arrays, they must be kept in sync with disk reality. Stale tests block CI for other contributors.
|
When APIs or public interfaces change, tests must be updated in the same commit. When test assertions reference file counts or expected arrays, they must be kept in sync with disk reality. Stale tests block CI for other contributors.
|
||||||
|
|
||||||
## Patterns
|
## Patterns
|
||||||
|
|
||||||
- **API changes → test updates (same commit):** If you change a function signature, public interface, or exported API, update the corresponding tests before committing
|
- **API changes → test updates (same commit):** If you change a function signature, public interface, or exported API, update the corresponding tests before committing
|
||||||
- **Test assertions → disk reality:** When test files contain expected counts (e.g., `EXPECTED_FEATURES`, `EXPECTED_SCENARIOS`), they must match the actual files on disk
|
- **Test assertions → disk reality:** When test files contain expected counts (e.g., `EXPECTED_FEATURES`, `EXPECTED_SCENARIOS`), they must match the actual files on disk
|
||||||
- **Add files → update assertions:** When adding docs pages, features, or any counted resource, update the test assertion array in the same commit
|
- **Add files → update assertions:** When adding docs pages, features, or any counted resource, update the test assertion array in the same commit
|
||||||
- **CI failures → check assertions first:** Before debugging complex failures, verify test assertion arrays match filesystem state
|
- **CI failures → check assertions first:** Before debugging complex failures, verify test assertion arrays match filesystem state
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
✓ **Correct:**
|
✓ **Correct:**
|
||||||
- Changed auth API signature → updated auth.test.ts in same commit
|
- Changed auth API signature → updated auth.test.ts in same commit
|
||||||
- Added `distributed-mesh.md` to features/ → added `'distributed-mesh'` to EXPECTED_FEATURES array
|
- Added `distributed-mesh.md` to features/ → added `'distributed-mesh'` to EXPECTED_FEATURES array
|
||||||
- Deleted two scenario files → removed entries from EXPECTED_SCENARIOS
|
- Deleted two scenario files → removed entries from EXPECTED_SCENARIOS
|
||||||
|
|
||||||
✗ **Incorrect:**
|
✗ **Incorrect:**
|
||||||
- Changed spawn parameters → committed without updating casting.test.ts (CI breaks for next person)
|
- Changed spawn parameters → committed without updating casting.test.ts (CI breaks for next person)
|
||||||
- Added `built-in-roles.md` → left EXPECTED_FEATURES at old count (PR blocked)
|
- Added `built-in-roles.md` → left EXPECTED_FEATURES at old count (PR blocked)
|
||||||
- Test says "expected 7 files" but disk has 25 (assertion staleness)
|
- Test says "expected 7 files" but disk has 25 (assertion staleness)
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
- Committing API changes without test updates ("I'll fix tests later")
|
- Committing API changes without test updates ("I'll fix tests later")
|
||||||
- Treating test assertion arrays as static (they evolve with content)
|
- Treating test assertion arrays as static (they evolve with content)
|
||||||
- Assuming CI passing means coverage is correct (stale assertions can pass while being wrong)
|
- Assuming CI passing means coverage is correct (stale assertions can pass while being wrong)
|
||||||
- Leaving gaps for other agents to discover
|
- Leaving gaps for other agents to discover
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user