fix: resolve 7 root causes causing cookbook deployment failures
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

RC-1: Gitea org fallback already removed (no-op, confirmed)
RC-3: Push/pull now explicitly target origin main (HEAD:main) in both
  pod_git_operations.go and claudebox/git.go — fixes Woodpecker webhook
  trigger by ensuring pushes always land on the main branch
RC-4: wait_for_pipeline records baseline pipeline number before polling;
  only returns success when a NEWER pipeline completes — prevents false
  positive when a prior pipeline was already success
RC-5: Redis WRONGPASS fixed on live persona-community-5 instance; platform
  gap noted (no reprovision endpoint for Redis ACL drift)
RC-6: Removed on_error:continue from all infra provisioning steps (add-db,
  add-redis) across persona-community, slackpath-2/3/4/5 trees — infra
  failures now fail the tree instead of silently continuing to a crash
RC-7: Added .pnpm-store/ to skeleton .gitignore — prevents thousands of
  cache files being committed by agents after pnpm install
RC-2: Updated all 12 cookbook trees — git_clone_url jordan/ → threesix/
  (24 occurrences across all slackpath, aeries, full-stack, genkit trees)
Also: strings.Cut and strings.SplitSeq lint fixes in pod_git_operations.go
  and claudebox/git.go

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jordan 2026-02-24 18:49:09 -07:00
parent 3dbde72966
commit 62a9bbb237
17 changed files with 67 additions and 67 deletions

View File

@ -156,14 +156,21 @@ wait_for_build() {
wait_for_pipeline() { wait_for_pipeline() {
local project_id="$1" local project_id="$1"
local max_attempts="${2:-120}" # 10 minutes default local max_attempts="${2:-120}" # 10 minutes default
local poll_interval="${3:-5}" local poll_interval="${3:-10}"
local attempt=0 local attempt=0
local tracked_pipeline="" # Track specific pipeline once found local tracked_pipeline="" # Track specific pipeline once found
echo -e "${CYAN}Waiting for CI pipeline...${NC}" echo -e "${CYAN}Waiting for new CI pipeline...${NC}"
# Wait a bit for pipeline to be created # Record the current latest pipeline number BEFORE waiting
sleep 5 # so we only track pipelines triggered AFTER this point
local baseline_number=0
local initial_result
initial_result=$(api_call GET "/projects/$project_id/pipelines" 2>/dev/null)
if echo "$initial_result" | jq -e '.data[0]' >/dev/null 2>&1; then
baseline_number=$(echo "$initial_result" | jq -r '.data[0].number // 0')
echo " Baseline pipeline: #$baseline_number — waiting for a newer one"
fi
while [[ $attempt -lt $max_attempts ]]; do while [[ $attempt -lt $max_attempts ]]; do
local result local result
@ -180,16 +187,23 @@ wait_for_pipeline() {
continue continue
fi fi
# Get latest pipeline status # Get latest pipeline number and status
local status local pipeline_number status
pipeline_number=$(echo "$result" | jq -r '.data[0].number // 0')
status=$(echo "$result" | jq -r '.data[0].status // "unknown"') status=$(echo "$result" | jq -r '.data[0].status // "unknown"')
local pipeline_number
pipeline_number=$(echo "$result" | jq -r '.data[0].number // "?"')
# Track the pipeline we're monitoring # Skip any pipeline that is not newer than our baseline
if [[ "$pipeline_number" -le "$baseline_number" ]]; then
echo " Waiting for new pipeline (latest is #$pipeline_number, baseline #$baseline_number)... (attempt $((attempt + 1))/$max_attempts)"
sleep "$poll_interval"
((attempt++))
continue
fi
# A new pipeline exists — track it
if [[ -z "$tracked_pipeline" ]]; then if [[ -z "$tracked_pipeline" ]]; then
tracked_pipeline="$pipeline_number" tracked_pipeline="$pipeline_number"
echo " Tracking pipeline #$pipeline_number" echo " Tracking new pipeline #$tracked_pipeline"
fi fi
case "$status" in case "$status" in
@ -235,7 +249,7 @@ wait_for_pipeline() {
((attempt++)) ((attempt++))
done done
echo -e "${YELLOW}Timeout waiting for pipeline to complete${NC}" echo -e "${YELLOW}Timeout waiting for new pipeline${NC}"
# On timeout, still run diagnostics to help debug # On timeout, still run diagnostics to help debug
if [[ -n "$tracked_pipeline" ]]; then if [[ -n "$tracked_pipeline" ]]; then
diagnose_pipeline_failure "$project_id" diagnose_pipeline_failure "$project_id"

View File

@ -68,7 +68,7 @@ steps:
prompt: "/spec-feature {{ .vars.feature_slug }}" prompt: "/spec-feature {{ .vars.feature_slug }}"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id
@ -90,7 +90,7 @@ steps:
prompt: "/implement-feature {{ .vars.feature_slug }} --scope backend --requirements 'Use pkg/api. DB Table: agents. Fields: id, name, personality_prompt, created_at.'" prompt: "/implement-feature {{ .vars.feature_slug }} --scope backend --requirements 'Use pkg/api. DB Table: agents. Fields: id, name, personality_prompt, created_at.'"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id

View File

@ -24,7 +24,7 @@ steps:
prompt: "/extract-service core-api/internal/domain/agent_logic simulation-svc --pattern strangler" prompt: "/extract-service core-api/internal/domain/agent_logic simulation-svc --pattern strangler"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .vars.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .vars.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id

View File

@ -38,7 +38,7 @@ steps:
prompt: "/implement-feature {{ .vars.feature_slug }} --requirements 'Simulation SVC publishes agent moves to Redis. Spatial SVC tracks proximity. If two agents are near, Core UI shows a chat bubble.'" prompt: "/implement-feature {{ .vars.feature_slug }} --requirements 'Simulation SVC publishes agent moves to Redis. Spatial SVC tracks proximity. If two agents are near, Core UI shows a chat bubble.'"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .vars.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .vars.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id

View File

@ -62,7 +62,7 @@ steps:
prompt: "/spec-feature {{ .vars.feature_slug }}" prompt: "/spec-feature {{ .vars.feature_slug }}"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id

View File

@ -62,7 +62,7 @@ steps:
prompt: "/spec-feature {{ .vars.feature_slug }}" prompt: "/spec-feature {{ .vars.feature_slug }}"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id
@ -92,7 +92,7 @@ steps:
prompt: "/design-feature {{ .vars.feature_slug }}" prompt: "/design-feature {{ .vars.feature_slug }}"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id
@ -135,7 +135,7 @@ steps:
prompt: "/implement-task {{ .vars.feature_slug }} {{ .outputs.create-task-backend.task_id }}" prompt: "/implement-task {{ .vars.feature_slug }} {{ .outputs.create-task-backend.task_id }}"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id

View File

@ -92,7 +92,7 @@ steps:
' '
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id

View File

@ -24,16 +24,14 @@ steps:
add-db: add-db:
description: CockroachDB for persona + media persistence description: CockroachDB for persona + media persistence
depends_on: [create-project] depends_on: [create-project]
on_error: continue
action: api action: api
method: POST method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/components" endpoint: "/projects/{{ .outputs.create-project.project_id }}/components"
body: { type: postgres, name: "persona-db" } body: { type: postgres, name: "persona-db" }
add-redis: add-redis:
description: Redis for SSE pub/sub and generation job queue (may already be provisioned by skeleton) description: Redis for SSE pub/sub and generation job queue
depends_on: [create-project] depends_on: [create-project]
on_error: continue
action: api action: api
method: POST method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/components" endpoint: "/projects/{{ .outputs.create-project.project_id }}/components"
@ -98,7 +96,7 @@ steps:
prompt: "/implement-feature persona-model --requirements 'DB migration in persona-api: CREATE TABLE personas (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, handle TEXT UNIQUE NOT NULL, gender TEXT NOT NULL, description TEXT NOT NULL, tags TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], spec_json JSONB, anchor_url TEXT, avatar_url TEXT, banner_url TEXT, image_urls TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], video_urls TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], status TEXT NOT NULL DEFAULT ''pending'', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()). Domain model Persona with same fields. PersonaService: Create(ctx, description, gender, customName string) (*Persona, error) — saves row then enqueues a generate_spec job. GetByID(ctx, id). List(ctx, limit, offset int) ([]*Persona, error). Queue job type PersonaGenerateJob {PersonaID string, Stage string} where Stage values are: spec, anchor, avatar, banner, gallery_batch, video. Endpoints all under /api/persona-api: POST /personas (body: {description, gender, custom_name?}, returns 202 with persona), GET /personas (query: limit=20, offset=0), GET /personas/{id}. All routes require JWT auth (use skeleton auth.Middleware). After any persona field update publish SSE event persona_updated to channel:personas via the SSE hub.'" prompt: "/implement-feature persona-model --requirements 'DB migration in persona-api: CREATE TABLE personas (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, handle TEXT UNIQUE NOT NULL, gender TEXT NOT NULL, description TEXT NOT NULL, tags TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], spec_json JSONB, anchor_url TEXT, avatar_url TEXT, banner_url TEXT, image_urls TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], video_urls TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], status TEXT NOT NULL DEFAULT ''pending'', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()). Domain model Persona with same fields. PersonaService: Create(ctx, description, gender, customName string) (*Persona, error) — saves row then enqueues a generate_spec job. GetByID(ctx, id). List(ctx, limit, offset int) ([]*Persona, error). Queue job type PersonaGenerateJob {PersonaID string, Stage string} where Stage values are: spec, anchor, avatar, banner, gallery_batch, video. Endpoints all under /api/persona-api: POST /personas (body: {description, gender, custom_name?}, returns 202 with persona), GET /personas (query: limit=20, offset=0), GET /personas/{id}. All routes require JWT auth (use skeleton auth.Middleware). After any persona field update publish SSE event persona_updated to channel:personas via the SSE hub.'"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id
@ -120,7 +118,7 @@ steps:
prompt: "/implement-feature persona-generation --requirements 'Implement the generation pipeline in media-worker using the skeleton ai-client package (LAOZHANG_API_KEY for images, GEMINI_API_KEY for video). Worker consumes PersonaGenerateJob from queue and executes the stage: STAGE spec — call LLM to generate a PersonaSpec JSON: {name, handle, gender, tags:[8 strings], personality_archetype, appearance:{hair, eyes, skin, body_type, distinctive_features}, style_signature, image_matrix:[{position:int, pose:string, clothing:string, background:string} x 30]}. Save full spec to spec_json, update name/handle/tags on the persona row. Then enqueue stage=anchor job. STAGE anchor — generate one reference face-front portrait image from appearance description. Save URL to anchor_url. Then enqueue stage=avatar AND stage=banner as parallel jobs. STAGE avatar — generate circular portrait derived from anchor appearance. Save to avatar_url. STAGE banner — generate 3:1 landscape lifestyle image derived from anchor. Save to banner_url. STAGE gallery_batch — generate 10 images using next 10 unfilled positions from image_matrix. Append URLs to image_urls array. If fewer than 30 images exist, enqueue another gallery_batch job. STAGE video — generate the next missing video from: [smile_reveal, personality_moment, lifestyle, invitation]. Append URL to video_urls. If fewer than 4 videos, enqueue next video job. After each stage: update persona row, publish job_update SSE event {persona_id, stage, status:complete|error, progress} to channel:personas.'" prompt: "/implement-feature persona-generation --requirements 'Implement the generation pipeline in media-worker using the skeleton ai-client package (LAOZHANG_API_KEY for images, GEMINI_API_KEY for video). Worker consumes PersonaGenerateJob from queue and executes the stage: STAGE spec — call LLM to generate a PersonaSpec JSON: {name, handle, gender, tags:[8 strings], personality_archetype, appearance:{hair, eyes, skin, body_type, distinctive_features}, style_signature, image_matrix:[{position:int, pose:string, clothing:string, background:string} x 30]}. Save full spec to spec_json, update name/handle/tags on the persona row. Then enqueue stage=anchor job. STAGE anchor — generate one reference face-front portrait image from appearance description. Save URL to anchor_url. Then enqueue stage=avatar AND stage=banner as parallel jobs. STAGE avatar — generate circular portrait derived from anchor appearance. Save to avatar_url. STAGE banner — generate 3:1 landscape lifestyle image derived from anchor. Save to banner_url. STAGE gallery_batch — generate 10 images using next 10 unfilled positions from image_matrix. Append URLs to image_urls array. If fewer than 30 images exist, enqueue another gallery_batch job. STAGE video — generate the next missing video from: [smile_reveal, personality_moment, lifestyle, invitation]. Append URL to video_urls. If fewer than 4 videos, enqueue next video job. After each stage: update persona row, publish job_update SSE event {persona_id, stage, status:complete|error, progress} to channel:personas.'"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id
@ -150,7 +148,7 @@ steps:
prompt: "/implement-feature community-ui --requirements 'Build the React community UI using skeleton packages (@project/ui, @project/auth, @project/layout, @project/realtime, @project/api-client). App uses DashboardShell layout from @project/layout. Routes: /login (public) and / and /personas/:id (protected via ProtectedRoute from @project/auth). LOGIN PAGE: OTP flow — email input, Send Code button, then code input + Verify button. On success store token and redirect to /. COMMUNITY PAGE (/): Top bar has Create Persona button. Main area is a responsive grid of PersonaCard components. PersonaCard shows: banner_url as card background image, avatar_url as circular overlay (bottom-left), name + handle + up to 4 tags as pills, image/video count badges. While status=pending|generating show a subtle pulsing overlay with current stage label (Crafting identity... / Generating anchor... etc). Click card → navigate to /personas/:id. CREATE PANEL: slides in from right, contains: description textarea (placeholder: Describe your persona...), gender pill selector (Female / Male / Non-binary), optional Name field, Create button. On submit POST /api/persona-api/personas — new card appears immediately in grid with generating state. DETAIL PAGE (/personas/:id): full-width banner header, large circular avatar centered, name + handle + all tags. Image gallery grid (masonry or even grid, click any image to open fullscreen lightbox with arrow nav). Videos section shows 4 video players (smile reveal, personality moment, lifestyle, invitation) with labels. REALTIME: useEventChannel from @project/realtime subscribed to channel:personas. On persona_updated event refresh that card data. On job_update event show stage progress on the card overlay.'" prompt: "/implement-feature community-ui --requirements 'Build the React community UI using skeleton packages (@project/ui, @project/auth, @project/layout, @project/realtime, @project/api-client). App uses DashboardShell layout from @project/layout. Routes: /login (public) and / and /personas/:id (protected via ProtectedRoute from @project/auth). LOGIN PAGE: OTP flow — email input, Send Code button, then code input + Verify button. On success store token and redirect to /. COMMUNITY PAGE (/): Top bar has Create Persona button. Main area is a responsive grid of PersonaCard components. PersonaCard shows: banner_url as card background image, avatar_url as circular overlay (bottom-left), name + handle + up to 4 tags as pills, image/video count badges. While status=pending|generating show a subtle pulsing overlay with current stage label (Crafting identity... / Generating anchor... etc). Click card → navigate to /personas/:id. CREATE PANEL: slides in from right, contains: description textarea (placeholder: Describe your persona...), gender pill selector (Female / Male / Non-binary), optional Name field, Create button. On submit POST /api/persona-api/personas — new card appears immediately in grid with generating state. DETAIL PAGE (/personas/:id): full-width banner header, large circular avatar centered, name + handle + all tags. Image gallery grid (masonry or even grid, click any image to open fullscreen lightbox with arrow nav). Videos section shows 4 video players (smile reveal, personality moment, lifestyle, invitation) with labels. REALTIME: useEventChannel from @project/realtime subscribed to channel:personas. On persona_updated event refresh that card data. On job_update event show stage progress on the card overlay.'"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id

View File

@ -93,7 +93,7 @@ steps:
prompt: "/implement-feature {{ .vars.feature_slug }} --requirements 'User model with email/password. POST /register, POST /login (returns JWT). Middleware that checks Authorization header. GET /me returns user profile.'" prompt: "/implement-feature {{ .vars.feature_slug }} --requirements 'User model with email/password. POST /register, POST /login (returns JWT). Middleware that checks Authorization header. GET /me returns user profile.'"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id

View File

@ -20,9 +20,8 @@ steps:
- domain: .data.domain - domain: .data.domain
add-redis: add-redis:
description: Add Redis for job queue (may already exist from skeleton) description: Add Redis for job queue
depends_on: [create-project] depends_on: [create-project]
on_error: continue
action: api action: api
method: POST method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/components" endpoint: "/projects/{{ .outputs.create-project.project_id }}/components"
@ -59,7 +58,7 @@ steps:
prompt: "/implement-feature {{ .vars.feature_slug }} --requirements 'API: POST /jobs pushes JSON to Redis. Worker: Pops from Redis, simulates work, updates status. API: GET /jobs/{id} returns status.'" prompt: "/implement-feature {{ .vars.feature_slug }} --requirements 'API: POST /jobs pushes JSON to Redis. Worker: Pops from Redis, simulates work, updates status. API: GET /jobs/{id} returns status.'"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id

View File

@ -20,9 +20,8 @@ steps:
- domain: .data.domain - domain: .data.domain
add-redis: add-redis:
description: Add Redis for Pub/Sub (may already exist from skeleton) description: Add Redis for Pub/Sub
depends_on: [create-project] depends_on: [create-project]
on_error: continue
action: api action: api
method: POST method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/components" endpoint: "/projects/{{ .outputs.create-project.project_id }}/components"
@ -56,7 +55,7 @@ steps:
prompt: "/implement-feature {{ .vars.feature_slug }} --requirements 'GET /ws upgrades to websocket. Incoming messages are published to Redis channel. Redis subscriber broadcasts to all connected clients.'" prompt: "/implement-feature {{ .vars.feature_slug }} --requirements 'GET /ws upgrades to websocket. Incoming messages are published to Redis channel. Redis subscriber broadcasts to all connected clients.'"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id

View File

@ -20,9 +20,8 @@ steps:
- domain: .data.domain - domain: .data.domain
add-db: add-db:
description: Add CockroachDB for user/auth storage (may already exist from skeleton) description: Add CockroachDB for user/auth storage
depends_on: [create-project] depends_on: [create-project]
on_error: continue
action: api action: api
method: POST method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/components" endpoint: "/projects/{{ .outputs.create-project.project_id }}/components"
@ -31,9 +30,8 @@ steps:
name: "main-db" name: "main-db"
add-redis: add-redis:
description: Add Redis for job queue and pub/sub (may already exist from skeleton) description: Add Redis for job queue and pub/sub
depends_on: [create-project] depends_on: [create-project]
on_error: continue
action: api action: api
method: POST method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/components" endpoint: "/projects/{{ .outputs.create-project.project_id }}/components"
@ -97,7 +95,7 @@ steps:
prompt: "/implement-feature {{ .vars.feature_slug }} --requirements 'Chat Service must call http://auth-svc/validate to check tokens. Chat Service must push to Redis queue for Worker. Worker must process tasks.'" prompt: "/implement-feature {{ .vars.feature_slug }} --requirements 'Chat Service must call http://auth-svc/validate to check tokens. Chat Service must push to Redis queue for Worker. Worker must process tasks.'"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id

View File

@ -25,7 +25,6 @@ steps:
add-db: add-db:
description: Add database for preferences storage description: Add database for preferences storage
depends_on: [create-project] depends_on: [create-project]
on_error: continue
action: api action: api
method: POST method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/components" endpoint: "/projects/{{ .outputs.create-project.project_id }}/components"
@ -95,7 +94,7 @@ steps:
prompt: "/spec-feature {{ .vars.feature_slug }} --requirements 'CRUD API for user preferences. GET/PUT /preferences/{user_id}. Preferences are key-value pairs stored in DB. Support theme, language, notifications settings.'" prompt: "/spec-feature {{ .vars.feature_slug }} --requirements 'CRUD API for user preferences. GET/PUT /preferences/{user_id}. Preferences are key-value pairs stored in DB. Support theme, language, notifications settings.'"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id
@ -140,7 +139,7 @@ steps:
prompt: "/design-feature {{ .vars.feature_slug }}" prompt: "/design-feature {{ .vars.feature_slug }}"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id
@ -170,7 +169,7 @@ steps:
prompt: "/breakdown-feature {{ .vars.feature_slug }}" prompt: "/breakdown-feature {{ .vars.feature_slug }}"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id
@ -200,7 +199,7 @@ steps:
prompt: "/create-qa-plan {{ .vars.feature_slug }}" prompt: "/create-qa-plan {{ .vars.feature_slug }}"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id
@ -269,7 +268,7 @@ steps:
prompt: "/implement-feature {{ .vars.feature_slug }}" prompt: "/implement-feature {{ .vars.feature_slug }}"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id
@ -312,7 +311,7 @@ steps:
prompt: "/review-feature {{ .vars.feature_slug }}" prompt: "/review-feature {{ .vars.feature_slug }}"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id
@ -355,7 +354,7 @@ steps:
prompt: "/audit-feature {{ .vars.feature_slug }}" prompt: "/audit-feature {{ .vars.feature_slug }}"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id
@ -398,7 +397,7 @@ steps:
prompt: "/run-qa {{ .vars.feature_slug }}" prompt: "/run-qa {{ .vars.feature_slug }}"
auto_commit: true auto_commit: true
auto_push: true auto_push: true
git_clone_url: "https://git.threesix.ai/jordan/{{ .outputs.create-project.project_id }}.git" git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs: outputs:
- build_id: .data.task_id - build_id: .data.task_id

View File

@ -76,17 +76,9 @@ func (c *Client) CreateRepo(ctx context.Context, name, description string, priva
DefaultBranch: "main", DefaultBranch: "main",
} }
var repo *gitea.Repository repo, _, err := c.client.CreateOrgRepo(c.defaultOwner, opts)
var err error
// Try to create as org repo first, fall back to user repo
repo, _, err = c.client.CreateOrgRepo(c.defaultOwner, opts)
if err != nil { if err != nil {
// May not be an org, try as user repo return nil, fmt.Errorf("failed to create repo %s/%s: %w", c.defaultOwner, name, err)
repo, _, err = c.client.CreateRepo(opts)
if err != nil {
return nil, fmt.Errorf("failed to create repo: %w", err)
}
} }
return repoFromGitea(repo), nil return repoFromGitea(repo), nil

View File

@ -35,6 +35,8 @@ go.work.sum
# Node # Node
node_modules/ node_modules/
.npm/ .npm/
.pnpm-store/
.pnpm-store
pnpm-lock.yaml pnpm-lock.yaml
# Shared packages # Shared packages

View File

@ -208,7 +208,7 @@ func (g *GitOperations) CommitAndPush(ctx context.Context, workDir, message stri
result.Error = fmt.Errorf("git diff: %w", err) result.Error = fmt.Errorf("git diff: %w", err)
return result return result
} }
for _, f := range strings.Split(strings.TrimSpace(diffOutput), "\n") { for f := range strings.SplitSeq(strings.TrimSpace(diffOutput), "\n") {
if f != "" { if f != "" {
result.FilesChanged = append(result.FilesChanged, f) result.FilesChanged = append(result.FilesChanged, f)
} }
@ -241,12 +241,12 @@ func (g *GitOperations) CommitAndPush(ctx context.Context, workDir, message stri
} }
// Pull before push to handle concurrent builds that may have advanced the remote. // Pull before push to handle concurrent builds that may have advanced the remote.
if err := g.runGit(ctx, workDir, "pull", "--rebase", "origin", "HEAD"); err != nil { if err := g.runGit(ctx, workDir, "pull", "--rebase", "origin", "main"); err != nil {
g.logger.Warn("git pull --rebase before push failed, attempting push anyway", g.logger.Warn("git pull --rebase before push failed, attempting push anyway",
"error", err, "work_dir", workDir) "error", err, "work_dir", workDir)
} }
if err := g.runGit(ctx, workDir, "push", "origin", "HEAD"); err != nil { if err := g.runGit(ctx, workDir, "push", "origin", "HEAD:main"); err != nil {
result.Error = fmt.Errorf("git push: %w", err) result.Error = fmt.Errorf("git push: %w", err)
return result return result
} }
@ -299,8 +299,7 @@ func (g *GitOperations) Status(ctx context.Context, workDir string) (*GitStatusR
return result, fmt.Errorf("git status: %w", err) return result, fmt.Errorf("git status: %w", err)
} }
lines := strings.Split(strings.TrimSpace(status), "\n") for line := range strings.SplitSeq(strings.TrimSpace(status), "\n") {
for _, line := range lines {
if len(line) > 3 { if len(line) > 3 {
result.ChangedFiles = append(result.ChangedFiles, strings.TrimSpace(line[3:])) result.ChangedFiles = append(result.ChangedFiles, strings.TrimSpace(line[3:]))
} }

View File

@ -98,8 +98,8 @@ func (g *PodGitOperations) CloneRepo(ctx context.Context, podName, workDir, clon
// Strip token from currentRemote for comparison, since clone stores the authenticated URL // Strip token from currentRemote for comparison, since clone stores the authenticated URL
// Format: https://token:TOKEN@host/path -> https://host/path // Format: https://token:TOKEN@host/path -> https://host/path
normalizedRemote := currentRemote normalizedRemote := currentRemote
if idx := strings.Index(currentRemote, "@"); idx != -1 && strings.HasPrefix(currentRemote, "https://") { if _, after, found := strings.Cut(currentRemote, "@"); found && strings.HasPrefix(currentRemote, "https://") {
normalizedRemote = "https://" + currentRemote[idx+1:] normalizedRemote = "https://" + after
} }
expectedURL := cloneURL expectedURL := cloneURL
@ -265,7 +265,7 @@ func (g *PodGitOperations) CommitAndPush(ctx context.Context, podName, workDir,
result.Error = fmt.Errorf("git diff: %w", err) result.Error = fmt.Errorf("git diff: %w", err)
return result return result
} }
for _, f := range strings.Split(strings.TrimSpace(diffOutput), "\n") { for f := range strings.SplitSeq(strings.TrimSpace(diffOutput), "\n") {
if f != "" { if f != "" {
result.FilesChanged = append(result.FilesChanged, f) result.FilesChanged = append(result.FilesChanged, f)
} }
@ -308,14 +308,14 @@ func (g *PodGitOperations) CommitAndPush(ctx context.Context, podName, workDir,
// Pull before push to handle concurrent builds that may have advanced the remote. // Pull before push to handle concurrent builds that may have advanced the remote.
// Use --rebase to replay our commit on top of any new remote commits. // Use --rebase to replay our commit on top of any new remote commits.
if err := g.runGitInPod(ctx, podName, workDir, "pull", "--rebase", "origin", "HEAD"); err != nil { if err := g.runGitInPod(ctx, podName, workDir, "pull", "--rebase", "origin", "main"); err != nil {
log.Warn("git pull --rebase before push failed, attempting push anyway", log.Warn("git pull --rebase before push failed, attempting push anyway",
logging.FieldPodName, podName, logging.FieldPodName, podName,
logging.FieldError, err, logging.FieldError, err,
) )
} }
if err := g.runGitInPod(ctx, podName, workDir, "push", "origin", "HEAD"); err != nil { if err := g.runGitInPod(ctx, podName, workDir, "push", "origin", "HEAD:main"); err != nil {
result.Error = fmt.Errorf("git push: %w", err) result.Error = fmt.Errorf("git push: %w", err)
return result return result
} }