🦞 ClawRecipes

Marketplace

Development Team

A small engineering team with a shared workspace (lead, dev, devops, test) using file-first tickets.

Team Recipev0.2.1teamengineeringqafile-first

Install / Scaffold

Copy-paste the command below in a terminal where OpenClaw is installed.

$ openclaw recipes scaffold-team development-team -t my-development-team --apply-config

Source

This is the recipe’s Markdown source.

---
id: development-team
name: Development Team
version: 0.2.1
description: A small engineering team with a shared workspace (lead, dev, devops, test) using file-first tickets.
kind: team
cronJobs:
  - id: lead-triage-loop
    name: "Lead triage loop"
    schedule: "*/30 7-23 * * 1-5"
    timezone: "America/New_York"
    agentId: "{{teamId}}-lead"
    timeoutSeconds: 1800
    message: |
      Lead triage loop: triage inbox/tickets, assign work, and update notes/status.md. Complete all pending triage before finishing.

      QA-gated PR rule:
      - Dev/DevOps must NOT open PRs.
      - QA (test) verifies first.
      - After `QA: PASS`, ticket stays in work/testing but is assigned `Owner: lead` (ready-for-pr).
      - Lead opens PR only for testing-lane tickets owned by lead with QA PASS evidence.

      CWD guardrail (team root): run:
        cd "$(bash ../../scripts/team-root.sh 2>/dev/null || bash ./scripts/team-root.sh)"
      before any relative-path commands (e.g. work/, notes/, scripts/).

      Anti-stuck: if lowest in-progress is HARD BLOCKED, advance the next unblocked ticket (or pull from backlog).
      If in-progress is stale (>12h no dated update), comment or move it back.
      Guardrail: run ./scripts/ticket-hygiene.sh each loop; if it fails, fix lane/status/owner mismatches before proceeding (assignment stubs are deprecated).

    enabledByDefault: true

  # Safe-idle role loops (enabled by default): roles do not "wake up" unless they have their own heartbeat schedule or cron.
  - id: dev-work-loop
    name: "Dev work loop (safe-idle)"
    schedule: "*/30 7-23 * * 1-5"
    timezone: "America/New_York"
    agentId: "{{teamId}}-dev"
    timeoutSeconds: 1800
    message: |
      Work loop: check for dev-assigned tickets. If you have one, complete it fully. If the task is too large for one session, complete a meaningful self-contained piece (not a fragment) and update the ticket with what's done and what remains. Always leave work in a clean, consistent state.

      Constraints:
      - Do NOT open PRs. Dev hands off to QA by moving ticket to work/testing and setting Owner=test.
      - Ensure ticket contains `## PR-ready` (repo + branch + sha) + verification steps.

    enabledByDefault: true

  - id: devops-work-loop
    name: "DevOps work loop (safe-idle)"
    schedule: "*/30 7-23 * * 1-5"
    timezone: "America/New_York"
    agentId: "{{teamId}}-devops"
    timeoutSeconds: 1800
    message: "Work loop: check for devops-assigned tickets/runs. If you have work, complete it fully. If the task is too large for one session, complete a meaningful self-contained piece and update the ticket with what's done and what remains. Write outputs under roles/devops/agent-outputs/."
    enabledByDefault: true

  - id: test-work-loop
    name: "Test/QA work loop (safe-idle)"
    schedule: "*/30 7-23 * * 1-5"
    timezone: "America/New_York"
    agentId: "{{teamId}}-test"
    timeoutSeconds: 1800
    message: |
      Work loop: drain work/testing tickets assigned to test. Complete each ticket's QA fully before moving on.

      Workflow:
      - On PASS: keep ticket in work/testing but set Owner=lead (ready-for-pr) and add a `QA: PASS` comment + evidence.
      - On FAIL: move back to work/in-progress, set Owner=dev, add `QA: FAIL` + repro.

      Constraints:
      - QA happens BEFORE PR creation.

    enabledByDefault: true

  # Optional generic executor loop (off by default): can be enabled later if you want an extra catch-all.
  - id: execution-loop
    name: "Execution loop"
    schedule: "*/30 7-23 * * 1-5"
    timezone: "America/New_York"
    agentId: "{{teamId}}-lead"
    timeoutSeconds: 1800
    message: |
      Execution loop: complete in-progress tickets and update notes/status.md. Finish each ticket fully before moving on. If a task is too large for one session, complete a meaningful self-contained piece and update the ticket with what's done and what remains.

      CWD guardrail (team root): run:
        cd "$(bash ../../scripts/team-root.sh 2>/dev/null || bash ./scripts/team-root.sh)"
      before any relative-path commands (e.g. work/, notes/, scripts/).

      Guardrail: run ./scripts/ticket-hygiene-dev.sh each loop; if it fails, fix lane/status/owner mismatches before proceeding (assignment stubs are deprecated).

      LEAD-OWNED TICKETS RULE (must follow)
      - Do NOT automatically move a ticket just because Owner=lead "expects backlog".
      - If you encounter a lead-owned ticket in work/in-progress or work/testing that seems misassigned:
        - LEAVE IT IN PLACE.
        - Add a dated comment to the ticket explaining what you observed and what should change.
        - If the ticket failed QA, set Owner=dev (and keep it in work/in-progress).

    enabledByDefault: false

  - id: workflow-runner-loop
    name: "Workflow runner loop (runs queue)"
    # 6-field cron with seconds; runs every 15s
    schedule: "*/5 * * * *"
    timezone: "UTC"
    agentId: "{{teamId}}-workflow-runner"
    message: |
      Workflow runner loop: claim + execute queued workflow runs for this team.

      Guardrails:
      - This loop should be safe to run frequently; keep it short and idempotent.
      - Do NOT execute runs without a valid lease/claim.

      Command:
        openclaw recipes workflows runner-tick --team-id {{teamId}} --concurrency 2 --lease-seconds 45

    enabledByDefault: false

  - id: pr-watcher
    name: "PR watcher (ticket-linked)"
    schedule: "*/30 7-23 * * 1-5"
    timezone: "America/New_York"
    agentId: "{{teamId}}-lead"
    message: |
      PR watcher (ticket-linked): scan active in-progress/testing tickets for GitHub PR URLs.

      Team-level default (recommended): on merge, MOVE ticket to TESTING (never DONE).
      Per-ticket override markers (literal strings, anywhere in the ticket body):
      - [pr-watcher:comment-only]       -> on merge, comment only (no lane move)
      - [pr-watcher:move-to-testing]    -> on merge, move to TESTING (if not already)
      - [pr-watcher:close]              -> on merge, ticket may be eligible for DONE (see guardrails)

      Always do:
      - Summarize checks/review/mergeable status in ticket comments.
      - If PR merged, comment "PR merged" + link + merge commit SHA (if available).

      Lane-move rules on merge:
      - NEVER move to DONE by default.
      - Only move to DONE if BOTH:
        1) ticket contains [pr-watcher:close]
        2) all Tasks checkboxes are completed ("- [x]" for every item under ## Tasks)
      - If DONE is NOT allowed, leave lane unchanged and comment WHY (missing marker and/or tasks incomplete).
      - If move-to-testing is selected (default or marker), move to TESTING when verification remains.

      Do NOT:
      - Do NOT create/update assignment stubs (work/assignments/* is deprecated).
    enabledByDefault: false

  - id: testing-lane-loop
    name: "Testing lane loop"
    schedule: "*/30 7-23 * * 1-5"
    timezone: "America/New_York"
    agentId: "{{teamId}}-test"
    message: |
      Testing lane loop (QA gate): drain work/testing tickets.

      Rules:
      - On PASS: keep lane as work/testing, set Owner=lead, and add `QA: PASS` + evidence.
      - On FAIL: move back to work/in-progress, set Owner=dev, add `QA: FAIL` + repro.
      - Do NOT move tickets to DONE on PASS. Lead will open PR after PASS.

    enabledByDefault: false

  - id: backup-devteam-work
    name: "Backup dev-team work (every 3h, off-hours avoided)"
    # Every 3h during 07:00-22:00 America/New_York (avoids 02:00-07:00 blackout)
    schedule: "0 7,10,13,16,19,22 * * *"
    timezone: "America/New_York"
    agentId: "{{teamId}}-lead"
    message: "Backup job: run ./scripts/backup-work.sh to create a timestamped tarball of work/notes/scripts."
    enabledByDefault: true
requiredSkills: []
team:
  teamId: development-team
agents:
  - role: lead
    name: Dev Team Lead
    tools:
      profile: "coding"
      allow: ["group:fs", "group:web", "group:runtime", "group:automation"]
      deny: []
  - role: dev
    name: Software Engineer
    tools:
      profile: "coding"
      allow: ["group:fs", "group:web", "group:runtime", "group:automation"]
      deny: []
  - role: devops
    name: DevOps / SRE
    tools:
      profile: "coding"
      allow: ["group:fs", "group:web", "group:runtime", "group:automation"]
      deny: []
  - role: test
    name: QA / Tester
    tools:
      profile: "coding"
      allow: ["group:fs", "group:web", "group:runtime"]
      deny: []

  - role: workflow-runner
    name: Workflow Runner
    tools:
      profile: "coding"
      allow: ["group:fs", "group:runtime", "group:automation"]
      deny: []

templates:
  sharedContext.ticketFlow: |
    {
      "laneByOwner": {
        "lead": "backlog",
        "dev": "in-progress",
        "devops": "in-progress",
        "test": "testing",
        "qa": "testing"
      },
      "defaultLane": "in-progress",
      "notes": [
        "Ready-for-PR state lives in work/testing but uses Owner: lead after QA PASS.",
        "ticket-hygiene.sh special-cases lead in testing to allow this without failing hygiene."
      ]
    }

  sharedContext.memoryPolicy: |
    # Team Memory Policy (File-first)

    Quick link: see `shared-context/MEMORY_PLAN.md` for the canonical "what goes where" map.

    This team is run **file-first**. Chat is not the system of record.

    ## Where memory lives (and what it's for)

    ### 1) Team knowledge memory (Kitchen UI)
    - `shared-context/memory/team.jsonl` (append-only)
    - `shared-context/memory/pinned.jsonl` (append-only)

    Kitchen's Team Editor → Memory tab reads/writes these JSONL streams.

    ### 2) Per-role continuity memory (agents)
    Each role keeps its own continuity memory:
    - `roles/<role>/memory/YYYY-MM-DD.md` (daily log)
    - `roles/<role>/MEMORY.md` (curated long-term memory)

    These files are what the role agent uses to "remember" decisions and context across sessions.

    ## Where to write things
    - Ticket = source of truth for a unit of work.
    - `../notes/plan.md` + `../shared-context/priorities.md` are **lead-curated**.
    - `../notes/status.md` is **append-only** and updated after each work session (3-5 bullets).

    ## Outputs / artifacts
    - Role-level raw output (append-only): `roles/<role>/agent-outputs/`
    - Team-level raw output (append-only, optional): `../shared-context/agent-outputs/`

    Guardrail: do **not** create or rely on `roles/<role>/shared-context/**`.

    ## Role work loop contract
    When a role's cron/heartbeat runs:
    - **No-op unless explicit queued work exists** for that role (ticket assigned/owned by role, or workflow run nodes assigned to the role agentId).
    - If work exists, **complete the ticket fully**. If the task is too large for one session, complete a meaningful self-contained piece (not a fragment) and update the ticket with what's done and what remains. Always leave work in a clean, consistent state.
    - Write back in this order:
      1) Update the relevant ticket(s) (source of truth).
      2) Append 1–3 bullets to `../notes/status.md` (team roll-up).
      3) Write raw logs/artifacts under `roles/<role>/agent-outputs/` and reference them from the ticket.
    - Team memory JSONL policy:
      - Non-lead roles must **not** write directly to `shared-context/memory/pinned.jsonl`.
      - Non-leads may propose memory items in ticket comments or role outputs; lead pins.
      - Optional: roles may append non-pinned learnings to dedicated streams (e.g. `shared-context/memory/<topic>.jsonl`) if the recipe/workflow opts in.

    ## End-of-session checklist (everyone)
    After meaningful work:
    1) Update the ticket with what changed + how to verify + rollback.
    2) Add a dated note in the ticket `## Comments`.
    3) Append 3-5 bullets to `../notes/status.md`.
    4) Append logs/output to `roles/<role>/agent-outputs/`.

  tickets: |
    # Tickets - {{teamId}}

    ## Workflow stages
    - backlog → in-progress → testing → done

    ## Roles / responsibility
    - dev/devops: implement + handoff to test (NO PR creation)
    - test: verify + record PASS/FAIL
    - lead: creates PR **only after QA PASS**

    ## "Ready for PR" (no extra lane)
    This team does **not** add a separate lane.

    Instead, a ticket is considered **ready for PR** when:
    - it is in `work/testing/`
    - `Owner: lead`
    - ticket contains a `QA: PASS` comment + evidence

    ## Handoff rules

    ### Dev → Test
    When implementation is ready:
    - Move ticket to `work/testing/`
    - Set `Owner: test`
    - Ensure the ticket contains:
      - verification steps ("How to test")
      - links to branch/commit under `## PR-ready`

    ### Test → Lead (QA PASS)
    On PASS:
    - Add a dated ticket comment: `QA: PASS` + evidence
    - Keep lane as `work/testing/`
    - Set `Owner: lead`

    On FAIL:
    - Add `QA: FAIL` + repro
    - Move ticket back to `work/in-progress/` and set `Owner: dev`

    ### Lead → PR
    Lead creates a PR only after QA PASS.
    When creating the PR:
    - Link the PR URL in the ticket
    - Add `[pr-watcher:close]` marker if the ticket is eligible to auto-move to DONE on merge.

    ## Required fields
    Each ticket must include:
    - Owner: lead|dev|devops|test
    - Status: backlog|in-progress|testing|done
    - Context
    - Requirements
    - Acceptance criteria


  sharedContext.qaAccess: |
    # QA Access - {{teamId}}

    This file exists to prevent QA tickets being bounced due to missing environment access.

    ## ClawKitchen (hosted)
    - URL: http://localhost:7777

    ### HTTP Basic Auth
    - Username: `kitchen`
    - Password: <AUTH_TOKEN> (obtain from your deployment secret / host config)

    ### QA token bootstrap
    Open once to set QA cookie:
    - http://localhost:7777/tickets?qaToken=<QA_TOKEN>

    ## Notes
    - Do NOT commit real credentials to git.
    - When a ticket requires hosted Kitchen verification, link this file from the ticket.

  sharedContext.plan: |
    # Plan (lead-curated)

    - (empty)

  sharedContext.status: |
    # Status (append-only)

    - (empty)

  sharedContext.memoryPlan: |
    # Memory Plan (Team)

    This team is file-first. Chat is not the system of record.

    ## Source of truth
    - Tickets (`work/*/*.md`) are the source of truth for a unit of work.

    ## Team knowledge memory (Kitchen UI)
    - `shared-context/memory/team.jsonl` (append-only)
    - `shared-context/memory/pinned.jsonl` (append-only, curated/high-signal)

    Policy:
    - Lead may pin to `pinned.jsonl`.
    - Non-leads propose memory items via ticket comments or role outputs; lead pins.

    ## Per-role continuity memory (agent startup)
    - `roles/<role>/MEMORY.md` (curated long-term)
    - `roles/<role>/memory/YYYY-MM-DD.md` (daily log)

    ## Plan vs status (team coordination)
    - `../notes/plan.md` + `../shared-context/priorities.md` are lead-curated
    - `../notes/status.md` is append-only roll-up (everyone appends)

    ## Outputs / artifacts
    - `roles/<role>/agent-outputs/` (append-only)
    - `../shared-context/agent-outputs/` (team-level, read/write from role via `../`)

    ## Role work loop contract
    - No-op unless explicit queued work exists for the role.
    - If work exists, complete it fully. If too large for one session, complete a meaningful self-contained piece and update the ticket with what's done and what remains.
    - Write back in order: ticket → `../notes/status.md` → `roles/<role>/agent-outputs/`.

  sharedContext.priorities: |
    # Priorities (lead-curated)

    - (empty)

  sharedContext.agentOutputsReadme: |
    # Agent Outputs (append-only)

    Put raw logs, command output, and investigation notes here.
    Prefer filenames like: `YYYY-MM-DD-topic.md`.


  sharedContext.teamRootScript: |
    #!/usr/bin/env bash
    set -euo pipefail

    # Team root resolver
    # Prints the absolute path to the team workspace root from any subdir (e.g. roles/<role>/).
    # Heuristic: find the nearest ancestor containing work/, roles/, and shared-context/.

    d="$(pwd -P)"
    while true; do
      if [[ -d "$d/work" && -d "$d/roles" && -d "$d/shared-context" ]]; then
        echo "$d"
        exit 0
      fi
      if [[ "$d" == "/" ]]; then
        echo "team-root.sh: could not find team root from $(pwd -P)" >&2
        exit 1
      fi
      d="$(dirname "$d")"
    done

  lead.ticketHygiene: |
    #!/usr/bin/env bash
    set -euo pipefail

    # ticket-hygiene.sh
    # Guardrail script used by lead triage + execution loops.
    # Assignment stubs are deprecated.
    #
    # Checks (ACTIVE lanes only):
    # - Ticket file location (lane) must match Status:
    # - Ticket Owner should be in the expected lane per shared-context/ticket-flow.json (best-effort)
    #
    # Notes:
    # - We intentionally do NOT enforce mapping for work/done/ because historical tickets may have old Owner/Status.

    cd "$(dirname "$0")/.."

    fail=0
    flow="shared-context/ticket-flow.json"

    lane_from_rel() {
      # expects work/<lane>/<file>.md
      echo "$1" | sed -E 's#^work/([^/]+)/.*$#\\1#'
    }

    field_from_md() {
      local file="$1"
      local key="$2"
      # Extract first matching header line like: Key: value
      local line
      line="$(grep -m1 -E "^${key}:[[:space:]]*" "$file" 2>/dev/null || true)"
      echo "${line#${key}:}" | sed -E 's/^\s+//'
    }

    expected_lane_for_owner() {
      local owner="$1"
      local currentLane="$2"

      # Special-case: lead may own BACKLOG (triage) OR TESTING (ready-for-pr) without hygiene failure.
      if [[ "$owner" == "lead" && ( "$currentLane" == "backlog" || "$currentLane" == "testing" ) ]]; then
        echo "$currentLane"
        return 0
      fi

      # If jq or the mapping file isn't available, do not block progress.
      if [[ ! -f "$flow" ]]; then
        echo "$currentLane"
        return 0
      fi
      if ! command -v jq >/dev/null 2>&1; then
        echo "$currentLane"
        return 0
      fi

      local out
      out="$(jq -r --arg o "$owner" '.laneByOwner[$o] // .defaultLane // empty' "$flow" 2>/dev/null || true)"
      if [[ -n "$out" ]]; then
        echo "$out"
      else
        echo "$currentLane"
      fi
    }

    check_ticket() {
      local file="$1"
      local rel="$file"
      rel="${rel#./}"

      local lane
      lane="$(lane_from_rel "$rel")"

      # Ignore done lane for owner/status enforcement.
      if [[ "$lane" == "done" ]]; then
        return 0
      fi

      local owner status
      owner="$(field_from_md "$file" "Owner")"
      status="$(field_from_md "$file" "Status")"

      if [[ -n "$status" && "$status" != "$lane" ]]; then
        echo "[FAIL] $rel: Status mismatch (has: $status, lane: $lane)" >&2
        fail=1
      fi

      if [[ -n "$owner" ]]; then
        local expected
        expected="$(expected_lane_for_owner "$owner" "$lane")"
        if [[ -n "$expected" && "$expected" != "$lane" ]]; then
          echo "[FAIL] $rel: Owner '$owner' expects lane '$expected' per $flow (currently in '$lane')" >&2
          fail=1
        fi
      fi
    }

    shopt -s nullglob
    for file in work/backlog/*.md work/in-progress/*.md work/testing/*.md work/done/*.md; do
      [[ -f "$file" ]] || continue
      check_ticket "$file"
    done

    if [[ "$fail" -ne 0 ]]; then
      exit 1
    fi

    echo "OK"

  lead.ticketHygieneDevShim: |
    #!/usr/bin/env bash
    set -euo pipefail
    # Compatibility shim: automation expects ticket-hygiene-dev.sh
    DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    exec "$DIR/ticket-hygiene.sh" "$@"

  lead.backupWork: |
    #!/usr/bin/env bash
    set -euo pipefail

    # Backup the dev-team ticket folders (work + notes + scripts) into a timestamped tarball.
    # Safe-by-default: never deletes tickets; only prunes old backup archives.

    ROOT="$HOME/.openclaw/workspace-{{teamId}}"
    OUTDIR="$HOME/.openclaw/workspace/_backups"
    mkdir -p "$OUTDIR"

    TS="$(date -u +%Y%m%dT%H%M%SZ)"
    OUT="$OUTDIR/workspace-{{teamId}}-${TS}.tgz"

    tar -czf "$OUT" -C "$ROOT" work notes scripts

    echo "$OUT"

    # Keep the most recent 60 backups (~7.5 days at 1 per 3h). Adjust as needed.
    ls -1t "$OUTDIR"/workspace-{{teamId}}-*.tgz 2>/dev/null | tail -n +61 | xargs -r rm -f

  # Expose the same root scripts under every role namespace
  # (scaffold-team applies the same `files:` list for each agent role).

  dev.ticketHygiene: |
    #!/usr/bin/env bash
    set -euo pipefail

    # ticket-hygiene.sh
    # Guardrail script used by lead triage + execution loops.
    # Assignment stubs are deprecated.
    #
    # Checks (ACTIVE lanes only):
    # - Ticket file location (lane) must match Status:
    # - Ticket Owner should be in the expected lane per shared-context/ticket-flow.json (best-effort)
    #
    # Notes:
    # - We intentionally do NOT enforce mapping for work/done/ because historical tickets may have old Owner/Status.

    cd "$(dirname "$0")/.."

    fail=0
    flow="shared-context/ticket-flow.json"

    lane_from_rel() {
      # expects work/<lane>/<file>.md
      echo "$1" | sed -E 's#^work/([^/]+)/.*$#\\1#'
    }

    field_from_md() {
      local file="$1"
      local key="$2"
      # Extract first matching header line like: Key: value
      local line
      line="$(grep -m1 -E "^${key}:[[:space:]]*" "$file" 2>/dev/null || true)"
      echo "${line#${key}:}" | sed -E 's/^\s+//'
    }

    expected_lane_for_owner() {
      local owner="$1"
      local currentLane="$2"

      # Special-case: lead may own BACKLOG (triage) OR TESTING (ready-for-pr) without hygiene failure.
      if [[ "$owner" == "lead" && ( "$currentLane" == "backlog" || "$currentLane" == "testing" ) ]]; then
        echo "$currentLane"
        return 0
      fi

      # If jq or the mapping file isn't available, do not block progress.
      if [[ ! -f "$flow" ]]; then
        echo "$currentLane"
        return 0
      fi
      if ! command -v jq >/dev/null 2>&1; then
        echo "$currentLane"
        return 0
      fi

      local out
      out="$(jq -r --arg o "$owner" '.laneByOwner[$o] // .defaultLane // empty' "$flow" 2>/dev/null || true)"
      if [[ -n "$out" ]]; then
        echo "$out"
      else
        echo "$currentLane"
      fi
    }

    check_ticket() {
      local file="$1"
      local rel="$file"
      rel="${rel#./}"

      local lane
      lane="$(lane_from_rel "$rel")"

      # Ignore done lane for owner/status enforcement.
      if [[ "$lane" == "done" ]]; then
        return 0
      fi

      local owner status
      owner="$(field_from_md "$file" "Owner")"
      status="$(field_from_md "$file" "Status")"

      if [[ -n "$status" && "$status" != "$lane" ]]; then
        echo "[FAIL] $rel: Status mismatch (has: $status, lane: $lane)" >&2
        fail=1
      fi

      if [[ -n "$owner" ]]; then
        local expected
        expected="$(expected_lane_for_owner "$owner" "$lane")"
        if [[ -n "$expected" && "$expected" != "$lane" ]]; then
          echo "[FAIL] $rel: Owner '$owner' expects lane '$expected' per $flow (currently in '$lane')" >&2
          fail=1
        fi
      fi
    }

    shopt -s nullglob
    for file in work/backlog/*.md work/in-progress/*.md work/testing/*.md work/done/*.md; do
      [[ -f "$file" ]] || continue
      check_ticket "$file"
    done

    if [[ "$fail" -ne 0 ]]; then
      exit 1
    fi

    echo "OK"

  dev.ticketHygieneDevShim: |
    #!/usr/bin/env bash
    set -euo pipefail
    # Compatibility shim: automation expects ticket-hygiene-dev.sh
    DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    exec "$DIR/ticket-hygiene.sh" "$@"

  dev.backupWork: |
    #!/usr/bin/env bash
    set -euo pipefail

    # Backup the dev-team ticket folders (work + notes + scripts) into a timestamped tarball.
    # Safe-by-default: never deletes tickets; only prunes old backup archives.

    ROOT="$HOME/.openclaw/workspace-{{teamId}}"
    OUTDIR="$HOME/.openclaw/workspace/_backups"
    mkdir -p "$OUTDIR"

    TS="$(date -u +%Y%m%dT%H%M%SZ)"
    OUT="$OUTDIR/workspace-{{teamId}}-${TS}.tgz"

    tar -czf "$OUT" -C "$ROOT" work notes scripts

    echo "$OUT"

    # Keep the most recent 60 backups (~7.5 days at 1 per 3h). Adjust as needed.
    ls -1t "$OUTDIR"/workspace-{{teamId}}-*.tgz 2>/dev/null | tail -n +61 | xargs -r rm -f

  workflow-runner.ticketHygiene: |
    #!/usr/bin/env bash
    set -euo pipefail

  workflow-runner.ticketHygieneDevShim: |
    #!/usr/bin/env bash
    set -euo pipefail
    # Compatibility shim: automation expects ticket-hygiene-dev.sh
    DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    exec "$DIR/ticket-hygiene.sh" "$@"

  workflow-runner.backupWork: |
    #!/usr/bin/env bash
    set -euo pipefail

  devops.ticketHygiene: |
    #!/usr/bin/env bash
    set -euo pipefail

    # ticket-hygiene.sh
    # Guardrail script used by lead triage + execution loops.
    # Assignment stubs are deprecated.
    #
    # Checks (ACTIVE lanes only):
    # - Ticket file location (lane) must match Status:
    # - Ticket Owner should be in the expected lane per shared-context/ticket-flow.json (best-effort)
    #
    # Notes:
    # - We intentionally do NOT enforce mapping for work/done/ because historical tickets may have old Owner/Status.

    cd "$(dirname "$0")/.."

    fail=0
    flow="shared-context/ticket-flow.json"

    lane_from_rel() {
      # expects work/<lane>/<file>.md
      echo "$1" | sed -E 's#^work/([^/]+)/.*$#\\1#'
    }

    field_from_md() {
      local file="$1"
      local key="$2"
      # Extract first matching header line like: Key: value
      local line
      line="$(grep -m1 -E "^${key}:[[:space:]]*" "$file" 2>/dev/null || true)"
      echo "${line#${key}:}" | sed -E 's/^\s+//'
    }

    expected_lane_for_owner() {
      local owner="$1"
      local currentLane="$2"

      # Special-case: lead may own BACKLOG (triage) OR TESTING (ready-for-pr) without hygiene failure.
      if [[ "$owner" == "lead" && ( "$currentLane" == "backlog" || "$currentLane" == "testing" ) ]]; then
        echo "$currentLane"
        return 0
      fi

      # If jq or the mapping file isn't available, do not block progress.
      if [[ ! -f "$flow" ]]; then
        echo "$currentLane"
        return 0
      fi
      if ! command -v jq >/dev/null 2>&1; then
        echo "$currentLane"
        return 0
      fi

      local out
      out="$(jq -r --arg o "$owner" '.laneByOwner[$o] // .defaultLane // empty' "$flow" 2>/dev/null || true)"
      if [[ -n "$out" ]]; then
        echo "$out"
      else
        echo "$currentLane"
      fi
    }

    check_ticket() {
      local file="$1"
      local rel="$file"
      rel="${rel#./}"

      local lane
      lane="$(lane_from_rel "$rel")"

      # Ignore done lane for owner/status enforcement.
      if [[ "$lane" == "done" ]]; then
        return 0
      fi

      local owner status
      owner="$(field_from_md "$file" "Owner")"
      status="$(field_from_md "$file" "Status")"

      if [[ -n "$status" && "$status" != "$lane" ]]; then
        echo "[FAIL] $rel: Status mismatch (has: $status, lane: $lane)" >&2
        fail=1
      fi

      if [[ -n "$owner" ]]; then
        local expected
        expected="$(expected_lane_for_owner "$owner" "$lane")"
        if [[ -n "$expected" && "$expected" != "$lane" ]]; then
          echo "[FAIL] $rel: Owner '$owner' expects lane '$expected' per $flow (currently in '$lane')" >&2
          fail=1
        fi
      fi
    }

    shopt -s nullglob
    for file in work/backlog/*.md work/in-progress/*.md work/testing/*.md work/done/*.md; do
      [[ -f "$file" ]] || continue
      check_ticket "$file"
    done

    if [[ "$fail" -ne 0 ]]; then
      exit 1
    fi

    echo "OK"

  devops.ticketHygieneDevShim: |
    #!/usr/bin/env bash
    set -euo pipefail
    # Compatibility shim: automation expects ticket-hygiene-dev.sh
    DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    exec "$DIR/ticket-hygiene.sh" "$@"

  devops.backupWork: |
    #!/usr/bin/env bash
    set -euo pipefail

    # Backup the dev-team ticket folders (work + notes + scripts) into a timestamped tarball.
    # Safe-by-default: never deletes tickets; only prunes old backup archives.

    ROOT="$HOME/.openclaw/workspace-{{teamId}}"
    OUTDIR="$HOME/.openclaw/workspace/_backups"
    mkdir -p "$OUTDIR"

    TS="$(date -u +%Y%m%dT%H%M%SZ)"
    OUT="$OUTDIR/workspace-{{teamId}}-${TS}.tgz"

    tar -czf "$OUT" -C "$ROOT" work notes scripts

    echo "$OUT"

    # Keep the most recent 60 backups (~7.5 days at 1 per 3h). Adjust as needed.
    ls -1t "$OUTDIR"/workspace-{{teamId}}-*.tgz 2>/dev/null | tail -n +61 | xargs -r rm -f

  test.ticketHygiene: |
    #!/usr/bin/env bash
    set -euo pipefail

    # ticket-hygiene.sh
    # Guardrail script used by lead triage + execution loops.
    # Assignment stubs are deprecated.
    #
    # Checks (ACTIVE lanes only):
    # - Ticket file location (lane) must match Status:
    # - Ticket Owner should be in the expected lane per shared-context/ticket-flow.json (best-effort)
    #
    # Notes:
    # - We intentionally do NOT enforce mapping for work/done/ because historical tickets may have old Owner/Status.

    cd "$(dirname "$0")/.."

    fail=0
    flow="shared-context/ticket-flow.json"

    lane_from_rel() {
      # expects work/<lane>/<file>.md
      echo "$1" | sed -E 's#^work/([^/]+)/.*$#\\1#'
    }

    field_from_md() {
      local file="$1"
      local key="$2"
      # Extract first matching header line like: Key: value
      local line
      line="$(grep -m1 -E "^${key}:[[:space:]]*" "$file" 2>/dev/null || true)"
      echo "${line#${key}:}" | sed -E 's/^\s+//'
    }

    expected_lane_for_owner() {
      local owner="$1"
      local currentLane="$2"

      # Special-case: lead may own BACKLOG (triage) OR TESTING (ready-for-pr) without hygiene failure.
      if [[ "$owner" == "lead" && ( "$currentLane" == "backlog" || "$currentLane" == "testing" ) ]]; then
        echo "$currentLane"
        return 0
      fi

      # If jq or the mapping file isn't available, do not block progress.
      if [[ ! -f "$flow" ]]; then
        echo "$currentLane"
        return 0
      fi
      if ! command -v jq >/dev/null 2>&1; then
        echo "$currentLane"
        return 0
      fi

      local out
      out="$(jq -r --arg o "$owner" '.laneByOwner[$o] // .defaultLane // empty' "$flow" 2>/dev/null || true)"
      if [[ -n "$out" ]]; then
        echo "$out"
      else
        echo "$currentLane"
      fi
    }

    check_ticket() {
      local file="$1"
      local rel="$file"
      rel="${rel#./}"

      local lane
      lane="$(lane_from_rel "$rel")"

      # Ignore done lane for owner/status enforcement.
      if [[ "$lane" == "done" ]]; then
        return 0
      fi

      local owner status
      owner="$(field_from_md "$file" "Owner")"
      status="$(field_from_md "$file" "Status")"

      if [[ -n "$status" && "$status" != "$lane" ]]; then
        echo "[FAIL] $rel: Status mismatch (has: $status, lane: $lane)" >&2
        fail=1
      fi

      if [[ -n "$owner" ]]; then
        local expected
        expected="$(expected_lane_for_owner "$owner" "$lane")"
        if [[ -n "$expected" && "$expected" != "$lane" ]]; then
          echo "[FAIL] $rel: Owner '$owner' expects lane '$expected' per $flow (currently in '$lane')" >&2
          fail=1
        fi
      fi
    }

    shopt -s nullglob
    for file in work/backlog/*.md work/in-progress/*.md work/testing/*.md work/done/*.md; do
      [[ -f "$file" ]] || continue
      check_ticket "$file"
    done

    if [[ "$fail" -ne 0 ]]; then
      exit 1
    fi

    echo "OK"

  test.ticketHygieneDevShim: |
    #!/usr/bin/env bash
    set -euo pipefail
    # Compatibility shim: automation expects ticket-hygiene-dev.sh
    DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    exec "$DIR/ticket-hygiene.sh" "$@"

  test.backupWork: |
    #!/usr/bin/env bash
    set -euo pipefail

    # Backup the dev-team ticket folders (work + notes + scripts) into a timestamped tarball.
    # Safe-by-default: never deletes tickets; only prunes old backup archives.

    ROOT="$HOME/.openclaw/workspace-{{teamId}}"
    OUTDIR="$HOME/.openclaw/workspace/_backups"
    mkdir -p "$OUTDIR"

    TS="$(date -u +%Y%m%dT%H%M%SZ)"
    OUT="$OUTDIR/workspace-{{teamId}}-${TS}.tgz"

    tar -czf "$OUT" -C "$ROOT" work notes scripts

    echo "$OUT"

    # Keep the most recent 60 backups (~7.5 days at 1 per 3h). Adjust as needed.
    ls -1t "$OUTDIR"/workspace-{{teamId}}-*.tgz 2>/dev/null | tail -n +61 | xargs -r rm -f

  lead.soul: |
    # SOUL.md

    You are the Team Lead / Dispatcher for {{teamId}}.

    Core job:
    - Convert new requests into scoped tickets.
    - Assign work to Dev or DevOps.
    - Monitor progress and unblock.
    - Report completions.

  lead.agents: |
    # AGENTS.md

    Team: {{teamId}}
    Shared workspace: {{teamDir}}

    ## Guardrails (read → act → write)

    Before you act:
    1) Read (role continuity):
       - `MEMORY.md`
       - `memory/YYYY-MM-DD.md` (today; create if missing)

    2) Read (team context):
       - `../notes/plan.md`
       - `../notes/status.md`
       - `../shared-context/priorities.md`
       - the relevant ticket(s)

    Optional (team knowledge memory, Kitchen UI):
       - `shared-context/memory/pinned.jsonl`
       - `shared-context/memory/team.jsonl`

    After you act:
    1) Write back:
       - Update tickets with decisions/assignments.
       - Keep `../notes/status.md` current (3-5 bullets per active ticket).

    ## Curator model

    You are the curator of:
    - `../notes/plan.md`
    - `../shared-context/priorities.md`

    Everyone else should append to:
    - `../shared-context/agent-outputs/` (append-only)
    - `shared-context/feedback/`

    Your job is to periodically distill those inputs into the curated files.

    ## File-first workflow (tickets)

    Source of truth is the shared team workspace.

    Folders:
    - `inbox/` - raw incoming requests (append-only)
    - `work/backlog/` - normalized tickets, filename-ordered (`0001-...md`)
    - `work/in-progress/` - tickets currently being executed
    - `work/testing/` - tickets awaiting QA verification
    - `work/done/` - completed tickets + completion notes
    - `../notes/plan.md` - current plan / priorities (curated)
    - `../notes/status.md` - current status snapshot
    - `shared-context/` - shared context + append-only outputs

    ### Ticket numbering (critical)
    - Backlog tickets MUST be named `0001-...md`, `0002-...md`, etc.
    - The developer pulls the lowest-numbered ticket assigned to them.

    ### Ticket format
    See `TICKETS.md` in the team root. Every ticket should include:
    - Context
    - Requirements
    - Acceptance criteria
    - Owner (dev/devops)
    - Status

    ### Your responsibilities
    - For every new request in `inbox/`, create a normalized ticket in `work/backlog/`.
    - Curate `../notes/plan.md` and `../shared-context/priorities.md`.
    - Keep `../notes/status.md` updated.
    - When work is ready for QA, move the ticket to `work/testing/` and assign it to the tester.
    - When QA passes a ticket, QA will keep it in `work/testing/` but set `Owner: lead` (ready-for-pr).
    - As lead, create the PR **only** for testing-lane tickets with `Owner: lead` + `QA: PASS` evidence.
      - PRs must stay specific to a single ticket/problem.
      - If a PR depends on another ticket/PR, the PR description must clearly say so (for example: `Depends on 0xxx`).
      - On PR creation, link PR in the ticket and add `[pr-watcher:close]` if eligible for auto-close on merge.
    - After PR merge, pr-watcher may move ticket to DONE if `[pr-watcher:close]` is present and tasks are complete.

  dev.soul: |
    # SOUL.md

    You are a Software Engineer on {{teamId}}.
    You implement features with clean, maintainable code and small PR-sized changes.

  dev.agents: |
    # AGENTS.md

    Shared workspace: {{teamDir}}

    ## Core workflow (QA gated)
    - Your job: implement changes and hand off to QA.
    - You MUST NOT open pull requests. PR creation is owned by the team lead after QA PASS.
    - When work is ready: move the ticket to `work/testing/` and set `Owner: test`.

    ## Guardrails (read → act → write)

    Before you change anything:
    1) Read (role continuity):
       - `MEMORY.md`
       - `memory/YYYY-MM-DD.md` (today; create if missing)

    2) Read (team context):
       - `../notes/plan.md`
       - `../notes/status.md`
       - `../shared-context/priorities.md`
       - the current ticket you're working on

    Optional (team knowledge memory, Kitchen UI):
       - `shared-context/memory/pinned.jsonl`
       - `shared-context/memory/team.jsonl`

    While working:
    - Keep changes small and safe.
    - Prefer file-first coordination over chat.

    After you finish a work session (even if not done):
    1) Write back:
       - Update the ticket with what you did and what's next.
       - Add **3-5 bullets** to `../notes/status.md` (what changed / what's blocked).
       - Append detailed output to `../shared-context/agent-outputs/` (append-only).

    Curator model:
    - Lead curates `../notes/plan.md` and `../shared-context/priorities.md`.
    - You should NOT edit curated files; propose changes via `agent-outputs/`.

    ## How you work (pull system)

    1) Look in `work/in-progress/` for any ticket already assigned to you.
       - If present: continue it.

    2) Otherwise, pick the next ticket from `work/backlog/`:
       - Choose the lowest-numbered `0001-...md` ticket assigned to `dev`.

    3) Move the ticket file from `work/backlog/` → `work/in-progress/`.

    4) Do the work.

    5) Handoff to QA (required):
       - Ensure the ticket has verification steps ("How to test").
       - Add a `## PR-ready` section with repo + branch + commit SHA (if known).
       - Move the ticket to `work/testing/`.
       - Set `Owner: test`.

    Notes:
    - Do NOT move tickets to `work/done/`.
    - Do NOT open PRs. Lead opens PR only after QA PASS.
    - Keep PR scope specific to the current ticket only.
    - If you discover a second issue, track it separately and keep it out of the current PR unless the dependency is explicit.
    - If one PR depends on another ticket/PR, make that dependency explicit in the PR description (for example: `Depends on 0xxx`).

  devops.soul: |
    # SOUL.md

    You are a DevOps/SRE on {{teamId}}.
    You focus on reliability, deployments, observability, and safe automation.

  devops.agents: |
    # AGENTS.md

    Shared workspace: {{teamDir}}

    ## Guardrails (read → act → write)

    Before you change anything:
    1) Read (role continuity):
       - `MEMORY.md`
       - `memory/YYYY-MM-DD.md` (today; create if missing)

    2) Read (team context):
       - `../notes/plan.md`
       - `../notes/status.md`
       - `../shared-context/priorities.md`
       - the current ticket you're working on

    Optional (team knowledge memory, Kitchen UI):
       - `shared-context/memory/pinned.jsonl`
       - `shared-context/memory/team.jsonl`

    After you finish a work session:
    1) Write back:
       - Update the ticket with what you did + verification steps.
       - Add **3-5 bullets** to `../notes/status.md`.
       - Append detailed output/logs to `../shared-context/agent-outputs/` (append-only).

    Curator model:
    - Lead curates `../notes/plan.md` and `../shared-context/priorities.md`.
    - You should NOT edit curated files; propose changes via `agent-outputs/`.

    ## How you work (pull system)

    1) Look in `work/in-progress/` for any ticket already assigned to you.
       - If present: continue it.

    2) Otherwise, pick the next ticket from `work/backlog/`:
       - Choose the lowest-numbered `0001-...md` ticket assigned to `devops`.

    3) Move the ticket file from `work/backlog/` → `work/in-progress/`.

    4) Do the work.

    5) Write a completion report into `work/done/` with:
       - What changed
       - How to verify
       - Rollback notes (if applicable)

  lead.tools: |
    # TOOLS.md

    # Agent-local notes for lead (paths, conventions, env quirks).

  lead.status: |
    # STATUS.md

    - (empty)

  lead.notes: |
    # NOTES.md

    - (empty)

  dev.tools: |
    # TOOLS.md

    # Agent-local notes for dev (paths, conventions, env quirks).

  dev.status: |
    # STATUS.md

    - (empty)

  dev.notes: |
    # NOTES.md

    - (empty)

  devops.tools: |
    # TOOLS.md

    # Agent-local notes for devops (paths, conventions, env quirks).

  devops.status: |
    # STATUS.md

    - (empty)

  devops.notes: |
    # NOTES.md

    - (empty)

  workflow-runner.soul: |
    # SOUL.md

    You are the Workflow Runner for {{teamId}}.
    Your job is to reliably execute queued workflow runs (file-first) and persist progress after each node.

  workflow-runner.agents: |
    # AGENTS.md

    Shared workspace: {{teamDir}}

    ## Startup (read)
    - `MEMORY.md`
    - `memory/YYYY-MM-DD.md` (today; create if missing)
    - `../notes/status.md` (for current known issues)

    Optional:
    - `shared-context/memory/pinned.jsonl`

    ## Primary responsibility
    Drain queued workflow runs without duplicating work:
    - Claim runs with a short lease
    - Execute up to the configured concurrency
    - Persist progress after each node
    - Skip runs awaiting approval (they do not consume execution capacity)

    ## How to operate
    - Prefer the CLI runner tick:
      `openclaw recipes workflows runner-tick --team-id {{teamId}} --concurrency 2 --lease-seconds 45`
    - If anything looks wrong (schema mismatch, repeated failures), STOP and write a note to `../notes/status.md`.

  workflow-runner.tools: |
    # TOOLS.md

    # Agent-local notes for workflow-runner.

  workflow-runner.status: |
    # STATUS.md

    - (empty)

  workflow-runner.notes: |
    # NOTES.md

    - (empty)

  test.soul: |
    # SOUL.md

    You are QA / Testing on {{teamId}}.

    Core job:
    - Verify completed work before it is marked done.
    - Run tests, try edge-cases, and confirm acceptance criteria.
    - If issues found: write a clear bug note and send the ticket back to in-progress.

  test.agents: |
    # AGENTS.md

    Shared workspace: {{teamDir}}

    ## Core workflow (QA gated)
    - You verify work before any PR is created.
    - If the ticket passes: keep it in `work/testing/` but set `Owner: lead` (this is the "ready for PR" state).
    - If it fails: move it back to `work/in-progress/` and set `Owner: dev`.

    ## Guardrails (read → act → write)

    Before verifying:
    1) Read (role continuity):
       - `MEMORY.md`
       - `memory/YYYY-MM-DD.md` (today; create if missing)

    2) Read (team context):
       - `../notes/plan.md`
       - `../notes/status.md`
       - `../shared-context/priorities.md`
       - the ticket under test

    Optional (team knowledge memory, Kitchen UI):
       - `shared-context/memory/pinned.jsonl`
       - `shared-context/memory/team.jsonl`

    After each verification pass:
    1) Write back:
       - Add a short verification note to the ticket (pass/fail + evidence).
       - Add **3-5 bullets** to `../notes/status.md` (what's verified / what's blocked).
       - Append detailed findings to `../shared-context/feedback/` or `../shared-context/agent-outputs/`.

    Curator model:
    - Lead curates `../notes/plan.md` and `../shared-context/priorities.md`.
    - You should NOT edit curated files; propose changes via feedback/outputs.

    ## How you work

    1) Look in `work/testing/` for tickets assigned to you.

    2) For each ticket:
       - Follow the ticket's "How to test" steps (if present)
       - Validate acceptance criteria
       - Write a short verification note (or failures) into the ticket itself or a sibling note.

    3) If it passes:
       - Add a dated ticket comment: `QA: PASS` + evidence (links, logs, screenshots as applicable).
       - Keep the ticket in `work/testing/`.
       - Set `Owner: lead` (this is the "ready for PR" state).

    4) If it fails:
       - Add a dated ticket comment: `QA: FAIL` + repro + what to fix.
       - Move the ticket back to `work/in-progress/`.
       - Set `Owner: dev`.

    ## Cleanup after testing

    If your test involved creating temporary resources (e.g., scaffolding test teams, creating test workspaces), **clean them up** after verification:

    1) Remove test workspaces:
       ```bash
       rm -rf ~/.openclaw/workspace-<test-team-id>
       ```

    2) Remove test agents from config (agents whose id starts with the test team id):
       - Edit `~/.openclaw/openclaw.json` and remove entries from `agents.list[]`
       - Or wait for `openclaw recipes remove-team` (once available)

    3) Remove any cron jobs created for the test team:
       ```bash
       openclaw cron list --all --json | grep "<test-team-id>"
       openclaw cron remove <jobId>
       ```

    4) Restart the gateway if you modified config:
       ```bash
       openclaw gateway restart
       ```

    **Naming convention:** When scaffolding test teams, use a prefix like `qa-<ticketNum>-` (e.g., `qa-0017-social-team`) so cleanup is easier.

  test.tools: |
    # TOOLS.md

    # Agent-local notes for test (paths, conventions, env quirks).

  test.status: |
    # STATUS.md

    - (empty)

  test.notes: |
    # NOTES.md

    - (empty)

files:
  - path: SOUL.md
    template: soul
    mode: createOnly
  - path: AGENTS.md
    template: agents
    mode: createOnly
  - path: TOOLS.md
    template: tools
    mode: createOnly
  - path: STATUS.md
    template: status
    mode: createOnly
  - path: NOTES.md
    template: notes
    mode: createOnly
  - path: shared-context/ticket-flow.json
    template: sharedContext.ticketFlow
    mode: createOnly

  # Memory / continuity (team-level)
  - path: notes/memory-policy.md
    template: sharedContext.memoryPolicy
    mode: createOnly
  - path: notes/QA_ACCESS.md
    template: sharedContext.qaAccess
    mode: createOnly
  - path: shared-context/MEMORY_PLAN.md
    template: sharedContext.memoryPlan
    mode: createOnly
  - path: notes/plan.md
    template: sharedContext.plan
    mode: createOnly
  - path: notes/status.md
    template: sharedContext.status
    mode: createOnly
  - path: shared-context/priorities.md
    template: sharedContext.priorities
    mode: createOnly
  - path: shared-context/agent-outputs/README.md
    template: sharedContext.agentOutputsReadme
    mode: createOnly


  # Automation / hygiene scripts
  # NOTE: portable policy: we do NOT chmod automatically. After scaffold:
  #   chmod +x scripts/*.sh
  - path: scripts/team-root.sh
    template: sharedContext.teamRootScript
  - path: scripts/ticket-hygiene.sh
    template: ticketHygiene
    mode: createOnly
  - path: scripts/ticket-hygiene-dev.sh
    template: ticketHygieneDevShim
    mode: createOnly
  - path: scripts/backup-work.sh
    template: backupWork
    mode: createOnly

tools:
  profile: "coding"
  allow: ["group:fs", "group:web"]
---
# Development Team Recipe

Scaffolds a shared team workspace and four namespaced agents (lead/dev/devops/test).

## What you get
- Shared workspace at `~/.openclaw/workspace-<teamId>/` (e.g. `~/.openclaw/workspace-development-team-team/`)
- File-first tickets: backlog → in-progress → testing → done
- Team lead acts as dispatcher; tester verifies before done

## Files
- Creates a shared team workspace under `~/.openclaw/workspace-<teamId>/` (example: `~/.openclaw/workspace-development-team-team/`).
- Creates per-role directories under `roles/<role>/` for: `SOUL.md`, `AGENTS.md`, `TOOLS.md`, `STATUS.md`, `NOTES.md`.
- Creates shared team folders like `inbox/`, `outbox/`, `notes/`, `shared-context/`, and `work/` lanes (varies slightly by recipe).

## Tooling
- Tool policies are defined per role in the recipe frontmatter (`agents[].tools`).
- Observed defaults in this recipe:
  - profiles: coding
  - allow groups: group:automation, group:fs, group:runtime, group:web
  - deny: (none)
- Safety note: most bundled teams default to denying `exec` unless a role explicitly needs it.