diff --git a/Makefile b/Makefile index 3d0889c8..f8e6f012 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help dev dev-all dev-down dev-all-down dev-all-reset dev-logs dev-status build test clean web-install dev-server dev-web build-web test-web typecheck-web lint-web generate-api db-reset validate-release-config staging staging-down staging-logs pr agent-worktrees agent-sync +.PHONY: help dev dev-all dev-down dev-all-down dev-all-reset dev-logs dev-status build test clean web-install dev-server dev-web build-web test-web typecheck-web lint-web generate-api db-reset validate-release-config staging staging-down staging-logs pr parallel-init parallel-sync parallel-up parallel-down DEV_DIR := .dev DEV_SERVER_PID := $(DEV_DIR)/server.pid @@ -11,8 +11,8 @@ STAGING_API_URL := http://localhost:8080 STAGING_WEB_URL := http://localhost STAGING_SERVER_IMAGE := skillhub-server:staging DEV_PROCESS := python3 scripts/dev_process.py -AGENT_BASE_REF ?= origin/main -AGENT_WORKTREE_ROOT ?= +PARALLEL_BASE_REF ?= origin/main +PARALLEL_WORKTREE_ROOT ?= DEV_COMPOSE_PROJECT_NAME ?= skillhub STAGING_COMPOSE_PROJECT_NAME ?= skillhub-staging DEV_COMPOSE := docker compose -p $(DEV_COMPOSE_PROJECT_NAME) @@ -257,16 +257,18 @@ pr: ## 推送当前分支并创建 Pull Request(需要 gh CLI,仅限交互 fi @gh pr create --fill --web || gh pr create --fill -agent-worktrees: ## 创建 Claude/Codex/integration 并行 worktree(TASK=) +parallel-init: ## 创建 Claude/Codex/integration 并行 worktree(TASK=) @if [ -z "$(TASK)" ]; then \ - echo "Usage: make agent-worktrees TASK= [AGENT_BASE_REF=origin/main] [AGENT_WORKTREE_ROOT=/path]"; \ + echo "Usage: make parallel-init TASK= [PARALLEL_BASE_REF=origin/main] [PARALLEL_WORKTREE_ROOT=/path]"; \ exit 1; \ fi - ./scripts/setup-agent-worktrees.sh "$(TASK)" "$(AGENT_BASE_REF)" "$(AGENT_WORKTREE_ROOT)" + ./scripts/parallel-init.sh "$(TASK)" "$(PARALLEL_BASE_REF)" "$(PARALLEL_WORKTREE_ROOT)" -agent-sync: ## 合并 agent 分支到 integration worktree(TASK= [SOURCES=\"branch1 branch2\"]) - @if [ -z "$(TASK)" ]; then \ - echo "Usage: make agent-sync TASK= [SOURCES=\"branch1 branch2\"]"; \ - exit 1; \ - fi - ./scripts/sync-agent-integration.sh "$(TASK)" $(SOURCES) +parallel-sync: ## 在 integration worktree 合并 Claude/Codex 分支(自动识别当前 task) + PARALLEL_WORKTREE_ROOT="$(PARALLEL_WORKTREE_ROOT)" ./scripts/parallel-sync.sh $(SOURCES) + +parallel-up: ## 在 integration worktree 合并并启动联调环境(自动识别当前 task) + PARALLEL_WORKTREE_ROOT="$(PARALLEL_WORKTREE_ROOT)" ./scripts/parallel-up.sh $(SOURCES) + +parallel-down: ## 在 integration worktree 停止联调环境 + ./scripts/parallel-down.sh diff --git a/docs/13-agent-parallel-workflow.md b/docs/13-parallel-workflow.md similarity index 72% rename from docs/13-agent-parallel-workflow.md rename to docs/13-parallel-workflow.md index 6cf5350c..4653f272 100644 --- a/docs/13-agent-parallel-workflow.md +++ b/docs/13-parallel-workflow.md @@ -2,6 +2,8 @@ This document defines the recommended way to run Claude and Codex in parallel on SkillHub without letting them overwrite each other. +Legacy aliases `agent-worktrees` and `agent-sync` still work as compatibility shims, but the canonical command set is `parallel-init`, `parallel-sync`, `parallel-up`, and `parallel-down`. + ## Goals - Keep Claude and Codex isolated while they write code. @@ -27,7 +29,7 @@ All worktrees share the same local Docker dependency project name, so `make dev` From the main repository: ```bash -make agent-worktrees TASK=legal-pages +make parallel-init TASK=legal-pages ``` This creates: @@ -45,7 +47,13 @@ And matching local branches: You can override the base branch or destination root: ```bash -make agent-worktrees TASK=legal-pages AGENT_BASE_REF=origin/main AGENT_WORKTREE_ROOT=/Users/wowo/workspace +make parallel-init TASK=legal-pages PARALLEL_BASE_REF=origin/main PARALLEL_WORKTREE_ROOT=/Users/wowo/workspace +``` + +If you are iterating on this workflow itself before it lands on `origin/main`, create the worktrees from your current branch instead: + +```bash +make parallel-init TASK=legal-pages PARALLEL_BASE_REF=HEAD ``` ## Recommended Responsibility Split @@ -70,21 +78,36 @@ Each agent should commit its own work before integration. ### 2. Merge into integration -From the main repository: +From the integration worktree: ```bash -make agent-sync TASK=legal-pages +cd ../skillhub-integration-legal-pages +make parallel-up ``` -This merges the default source branches: +This does both routine steps in one command: + +- merges the default source branches into the current integration branch +- starts the integration worktree with `make dev-all` + +The default source branches are: - `agent/claude/legal-pages` - `agent/codex/legal-pages` -If needed, you can override the source list: +If you need manual control, you can still split the flow: ```bash -make agent-sync TASK=legal-pages SOURCES="agent/claude/legal-pages agent/codex/legal-pages" +cd ../skillhub-integration-legal-pages +make parallel-sync +make dev-all +``` + +If needed, you can override the source list on either command: + +```bash +make parallel-sync SOURCES="agent/claude/legal-pages agent/codex/legal-pages" +make parallel-up SOURCES="agent/claude/legal-pages agent/codex/legal-pages" ``` If a merge conflict happens, resolve it in the integration worktree. Do not resolve it inside the Claude or Codex worktree unless you intentionally want to rewrite that branch. @@ -95,7 +118,7 @@ Run the local stack only in the integration worktree: ```bash cd ../skillhub-integration-legal-pages -make dev-all +make parallel-up ``` Then verify the merged result in the browser: @@ -106,7 +129,7 @@ Then verify the merged result in the browser: When you are done: ```bash -make dev-all-down +make parallel-down ``` ## Validation Checklist @@ -114,7 +137,7 @@ make dev-all-down Before opening a PR from the integration branch: 1. Run the smallest relevant local verification first, for example `pnpm --dir web typecheck` or backend tests. -2. Start the integration stack with `make dev-all`. +2. From the integration worktree, run `make parallel-up`. 3. Verify the final merged behavior in `http://localhost:3000`. 4. Run `make staging` if the change needs Docker-path or smoke-test confidence. @@ -135,18 +158,16 @@ For this repository, the simplest path is usually: 1. Claude branch commits 2. Codex branch commits -3. Merge both into integration +3. In the integration worktree, run `make parallel-up` 4. Verify on `localhost:3000` 5. Open the PR from integration ## Commands Summary ```bash -make agent-worktrees TASK=legal-pages -make agent-sync TASK=legal-pages - +make parallel-init TASK=legal-pages cd ../skillhub-integration-legal-pages -make dev-all +make parallel-up open http://localhost:3000 -make dev-all-down +make parallel-down ``` diff --git a/docs/dev-workflow.md b/docs/dev-workflow.md index f6a592fb..d982dc44 100644 --- a/docs/dev-workflow.md +++ b/docs/dev-workflow.md @@ -61,22 +61,31 @@ Two mock users are available in local mode (no password needed): When two agents need to work in parallel, do not point both of them at the same checkout. Create isolated task worktrees instead: ```bash -make agent-worktrees TASK=legal-pages +make parallel-init TASK=legal-pages ``` -That creates dedicated Claude, Codex, and integration worktrees as sibling directories. Keep `localhost:3000` reserved for the integration worktree only: +That creates dedicated Claude, Codex, and integration worktrees as sibling directories. Keep `localhost:3000` reserved for the integration worktree only. + +After the one-time setup, switch to the integration worktree for the daily merge + verification loop: ```bash -make agent-sync TASK=legal-pages cd ../skillhub-integration-legal-pages -make dev-all +make parallel-up ``` Then verify the merged result at http://localhost:3000. Because all worktrees share the same local dependency project, you only need one set of Postgres, Redis, and MinIO containers for all of them. -See [13-agent-parallel-workflow.md](./13-agent-parallel-workflow.md) for the full workflow, responsibilities, merge rules, and recovery guidance. +If you need to inspect or resolve merge conflicts before starting the app, you can still split the flow manually: + +```bash +cd ../skillhub-integration-legal-pages +make parallel-sync +make dev-all +``` + +See [13-parallel-workflow.md](./13-parallel-workflow.md) for the full workflow, responsibilities, merge rules, and recovery guidance. ## Stage 2: Staging Regression (pre-PR validation) diff --git a/scripts/parallel-common.sh b/scripts/parallel-common.sh new file mode 100644 index 00000000..0669ca8f --- /dev/null +++ b/scripts/parallel-common.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +fail() { + echo "ERROR: $*" >&2 + exit 1 +} + +info() { + echo "INFO: $*" +} + +slugify() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' +} + +repo_root() { + git rev-parse --show-toplevel 2>/dev/null || fail "Run this script inside a git repository" +} + +git_common_dir() { + local root common_dir + root="$(repo_root)" + common_dir="$(git rev-parse --git-common-dir 2>/dev/null)" || fail "Unable to resolve the git common directory" + + if [ "${common_dir#/}" != "$common_dir" ]; then + printf '%s\n' "$common_dir" + return 0 + fi + + ( + cd "$root/$common_dir" && pwd + ) +} + +repo_name() { + basename "$(dirname "$(git_common_dir)")" +} + +current_branch() { + git rev-parse --abbrev-ref HEAD 2>/dev/null || fail "Unable to resolve the current branch" +} + +integration_task_from_branch() { + local branch="$1" + + case "$branch" in + agent/integration/*) + printf '%s\n' "${branch#agent/integration/}" + ;; + *) + return 1 + ;; + esac +} + +require_integration_task() { + local branch task + branch="$(current_branch)" + task="$(integration_task_from_branch "$branch")" || fail "Run this command inside the integration worktree on branch agent/integration/" + printf '%s\n' "$task" +} + +worktree_root() { + local root="${1:-$(repo_root)}" + printf '%s\n' "${PARALLEL_WORKTREE_ROOT:-$(dirname "$root")}" +} + +integration_dir_for_task() { + local task="$1" + local root + root="$(repo_root)" + printf '%s/%s-integration-%s\n' "$(worktree_root "$root")" "$(repo_name)" "$task" +} diff --git a/scripts/parallel-down.sh b/scripts/parallel-down.sh new file mode 100755 index 00000000..2b7d46f4 --- /dev/null +++ b/scripts/parallel-down.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./parallel-common.sh +source "$SCRIPT_DIR/parallel-common.sh" + +usage() { + cat <<'EOF' +Usage: parallel-down.sh + +Stops the integration worktree development stack. + +Run this inside an integration worktree on branch agent/integration/. +EOF +} + +if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + usage + exit 0 +fi + +TASK_SLUG="$(require_integration_task)" +REPO_ROOT="$(repo_root)" + +info "Stopping integration stack for task $TASK_SLUG in $REPO_ROOT" +make -C "$REPO_ROOT" dev-all-down diff --git a/scripts/setup-agent-worktrees.sh b/scripts/parallel-init.sh similarity index 78% rename from scripts/setup-agent-worktrees.sh rename to scripts/parallel-init.sh index b74dd3a9..01a5bd54 100755 --- a/scripts/setup-agent-worktrees.sh +++ b/scripts/parallel-init.sh @@ -1,9 +1,13 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./parallel-common.sh +source "$SCRIPT_DIR/parallel-common.sh" + usage() { cat <<'EOF' -Usage: setup-agent-worktrees.sh [base-ref] [worktree-root] +Usage: parallel-init.sh [base-ref] [worktree-root] Creates three sibling git worktrees for parallel Claude + Codex development: - -claude- @@ -16,23 +20,10 @@ Arguments: worktree-root Optional parent directory for new worktrees Example: - ./scripts/setup-agent-worktrees.sh legal-pages origin/main /Users/me/workspace + ./scripts/parallel-init.sh legal-pages origin/main /Users/me/workspace EOF } -fail() { - echo "ERROR: $*" >&2 - exit 1 -} - -info() { - echo "INFO: $*" -} - -slugify() { - printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' -} - branch_in_use() { local branch="$1" git worktree list --porcelain | awk '/^branch / { print $2 }' | grep -Fxq "refs/heads/$branch" @@ -66,6 +57,11 @@ ensure_worktree() { fi } +if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + usage + exit 0 +fi + TASK_INPUT="${1:-}" BASE_REF="${2:-origin/main}" WORKTREE_ROOT="${3:-}" @@ -75,8 +71,8 @@ if [ -z "$TASK_INPUT" ]; then exit 1 fi -REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || fail "Run this script inside a git repository" -REPO_NAME="$(basename "$REPO_ROOT")" +REPO_ROOT="$(repo_root)" +REPO_NAME="$(repo_name)" TASK_SLUG="$(slugify "$TASK_INPUT")" if [ -z "$TASK_SLUG" ]; then @@ -114,10 +110,9 @@ Parallel worktrees are ready: Recommended flow: 1. Run Claude only in: $CLAUDE_DIR 2. Run Codex only in: $CODEX_DIR - 3. Keep localhost:3000 reserved for the integration worktree: - cd "$INTEGRATION_DIR" && make dev-all - 4. After each agent commits, sync both branches into integration: - make agent-sync TASK=$TASK_SLUG - 5. Verify the merged result in the browser: + 3. Work only in the integration worktree when you need merged verification: + cd "$INTEGRATION_DIR" + make parallel-up + 4. Verify the merged result in the browser: http://localhost:3000 EOF diff --git a/scripts/sync-agent-integration.sh b/scripts/parallel-sync.sh similarity index 64% rename from scripts/sync-agent-integration.sh rename to scripts/parallel-sync.sh index 390bb119..00c6d496 100755 --- a/scripts/sync-agent-integration.sh +++ b/scripts/parallel-sync.sh @@ -1,37 +1,33 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./parallel-common.sh +source "$SCRIPT_DIR/parallel-common.sh" + usage() { cat <<'EOF' -Usage: sync-agent-integration.sh [source-branch...] +Usage: parallel-sync.sh [task-slug] [source-branch...] Merges Claude and Codex task branches into the matching integration worktree. Arguments: - task-slug Required task identifier, for example legal-pages + task-slug Optional task identifier, for example legal-pages. + When omitted, infer it from the current integration branch. source-branch Optional source branches. Defaults to: agent/claude/ agent/codex/ Example: - ./scripts/sync-agent-integration.sh legal-pages - ./scripts/sync-agent-integration.sh legal-pages agent/claude/legal-pages agent/codex/legal-pages + ./scripts/parallel-sync.sh legal-pages + cd ../skillhub-integration-legal-pages && ./scripts/parallel-sync.sh + ./scripts/parallel-sync.sh legal-pages agent/claude/legal-pages agent/codex/legal-pages + +Environment: + PARALLEL_WORKTREE_ROOT Optional parent directory for parallel worktrees EOF } -fail() { - echo "ERROR: $*" >&2 - exit 1 -} - -info() { - echo "INFO: $*" -} - -slugify() { - printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' -} - require_clean_worktree() { local dir="$1" if ! git -C "$dir" diff --quiet || ! git -C "$dir" diff --cached --quiet; then @@ -39,19 +35,21 @@ require_clean_worktree() { fi } -TASK_INPUT="${1:-}" -shift || true - -if [ -z "$TASK_INPUT" ]; then - usage >&2 - exit 1 +if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + usage + exit 0 fi -REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || fail "Run this script inside a git repository" -REPO_NAME="$(basename "$REPO_ROOT")" -TASK_SLUG="$(slugify "$TASK_INPUT")" -WORKTREE_ROOT="$(dirname "$REPO_ROOT")" -INTEGRATION_DIR="$WORKTREE_ROOT/${REPO_NAME}-integration-$TASK_SLUG" +TASK_INPUT="${1:-}" +if [ -n "$TASK_INPUT" ] && [[ "$TASK_INPUT" != */* ]]; then + shift + TASK_SLUG="$(slugify "$TASK_INPUT")" +else + TASK_SLUG="$(require_integration_task)" +fi + +REPO_ROOT="$(repo_root)" +INTEGRATION_DIR="$(integration_dir_for_task "$TASK_SLUG")" INTEGRATION_BRANCH="agent/integration/$TASK_SLUG" if [ ! -e "$INTEGRATION_DIR/.git" ]; then diff --git a/scripts/parallel-up.sh b/scripts/parallel-up.sh new file mode 100755 index 00000000..cf30fe91 --- /dev/null +++ b/scripts/parallel-up.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./parallel-common.sh +source "$SCRIPT_DIR/parallel-common.sh" + +usage() { + cat <<'EOF' +Usage: parallel-up.sh [task-slug] [source-branch...] + +Syncs Claude and Codex into the integration worktree, then starts `make dev-all`. + +Arguments: + task-slug Optional task identifier. When omitted, infer it from the + current integration branch. + source-branch Optional source branches passed through to parallel-sync.sh + +Examples: + cd ../skillhub-integration-legal-pages && ./scripts/parallel-up.sh + ./scripts/parallel-up.sh legal-pages +EOF +} + +if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + usage + exit 0 +fi + +TASK_INPUT="${1:-}" +if [ -n "$TASK_INPUT" ] && [[ "$TASK_INPUT" != */* ]]; then + shift + TASK_SLUG="$(slugify "$TASK_INPUT")" +else + TASK_SLUG="$(require_integration_task)" +fi + +REPO_ROOT="$(repo_root)" +INTEGRATION_DIR="$(integration_dir_for_task "$TASK_SLUG")" + +if [ ! -e "$INTEGRATION_DIR/.git" ]; then + fail "Integration worktree not found: $INTEGRATION_DIR" +fi + +PARALLEL_WORKTREE_ROOT="$(worktree_root "$REPO_ROOT")" "$REPO_ROOT/scripts/parallel-sync.sh" "$TASK_SLUG" "$@" +info "Starting integration stack in $INTEGRATION_DIR" +make -C "$INTEGRATION_DIR" dev-all + +cat <