# formulas/run-gardener.toml — Gardener housekeeping formula
#
# Defines the gardener's complete run: grooming (Claude session via
# gardener-run.sh) + AGENTS.md maintenance + final commit-and-pr.
#
# Gardener has journaling via .profile (issue #97), so it learns from
# past runs and improves over time.
#
# Steps: preflight -> grooming -> dust-bundling -> agents-update -> commit-and-pr

name        = "run-gardener"
description = "Mechanical housekeeping: grooming, dust bundling, docs update"
version     = 1

[context]
files = ["AGENTS.md", "VISION.md", "README.md"]

# ─────────────────────────────────────────────────────────────────────
# Step 1: preflight
# ─────────────────────────────────────────────────────────────────────

[[steps]]
id    = "preflight"
title = "Pull latest code"
description = """
Set up the working environment for this gardener run.

1. Change to the project repository:
     cd "$PROJECT_REPO_ROOT"

2. Pull the latest code:
     git fetch origin "$PRIMARY_BRANCH" --quiet
     git checkout "$PRIMARY_BRANCH" --quiet
     git pull --ff-only origin "$PRIMARY_BRANCH" --quiet

3. Record the current HEAD SHA for AGENTS.md watermarks:
     HEAD_SHA=$(git rev-parse HEAD)
     echo "$HEAD_SHA" > /tmp/gardener-head-sha

4. Initialize the pending-actions manifest (JSONL, converted to JSON at commit time):
     printf '' > "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
"""

# ─────────────────────────────────────────────────────────────────────
# Step 2: grooming — Claude-driven backlog grooming
# ─────────────────────────────────────────────────────────────────────

[[steps]]
id    = "grooming"
title = "Backlog grooming — triage all open issues"
description = """
Groom the open issue backlog. This step is the core Claude-driven analysis
(Claude performs pre-checks inline before deeper analysis).

Pre-checks (bash, zero tokens — detect problems before invoking Claude):

1. Fetch all open issues:
     curl -sf -H "Authorization: token $FORGE_TOKEN" \
       "$FORGE_API/issues?state=open&type=issues&limit=50&sort=updated&direction=desc"

2. Duplicate detection: compare issue titles pairwise. Normalize
   (lowercase, strip prefixes like feat:/fix:/refactor:, collapse whitespace)
   and flag pairs with >60% word overlap as possible duplicates.

3. Missing acceptance criteria: flag issues with body < 100 chars and
   no checkboxes (- [ ] or - [x]).

4. Stale issues: flag issues with no update in 14+ days.

5. Blockers starving the factory (HIGHEST PRIORITY): find issues that
   block backlog items but are NOT themselves labeled backlog. These
   starve the dev-agent completely. Extract deps from ## Dependencies /
   ## Depends on / ## Blocked by sections of backlog issues and check
   if each dependency is open + not backlog-labeled.

6. Tech-debt promotion: list all tech-debt labeled issues — goal is to
   process them all (promote to backlog or classify as dust).

7. Bug-report detection: for each open unlabeled issue (no backlog, no
   bug-report, no in-progress, no blocked, no underspecified, no vision,
   no tech-debt), check whether it describes a user-facing bug with
   reproduction steps. Criteria — ALL must be true:
   a. Body describes broken behavior (something that should work but
      doesn't), NOT a feature request or enhancement
   b. Body contains steps to reproduce (numbered list, "steps to
      reproduce" heading, or clear sequence of actions that trigger the bug)
   c. Issue is not already labeled

   If all criteria match, enrich the issue body and write the manifest actions:

   Body enrichment (CRITICAL — turns raw reports into actionable investigation briefs):
   Before writing the add_label action, construct an enriched body by appending
   these sections to the original issue body:

   a. ``## What was reported``
      One or two sentence summary of the user's claim. Distill the broken
      behavior concisely — what the user expected vs. what actually happened.

   b. ``## Known context``
      What can be inferred from the codebase without running anything:
      - Which contracts/components/files are involved (use AGENTS.md layout
        and file paths mentioned in the issue or body)
      - What the expected behavior should be (from VISION.md, docs, code)
      - Any recent changes to involved components:
          git log --oneline -5 -- <paths>
      - Related issues or prior fixes (cross-reference by number if known)

   c. ``## Reproduction plan``
      Concrete steps for a reproduce-agent or human. Be specific:
      - Which environment to use (e.g. "start fresh stack with
        \`./scripts/dev.sh restart --full\`")
      - Which transactions or actions to execute (with \`cast\` commands,
        API calls, or UI navigation steps where applicable)
      - What state to check after each step (contract reads, API queries,
        UI observations, log output)

   d. ``## What needs verification``
      Checkboxes distinguishing known facts from unknowns:
      - ``- [ ]`` Does the reported behavior actually occur? (reproduce)
      - ``- [ ]`` Is <component X> behaving as expected? (check state)
      - ``- [ ]`` Is the data flow correct from <A> to <B>? (trace)
      Tailor these to the specific bug — three to five items covering the
      key unknowns a reproduce-agent must resolve.

   e. Construct full new body = original body text + appended sections.
      Write an edit_body action BEFORE the add_label action:
        echo '{"action":"edit_body","issue":NNN,"body":"<full new body>"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"

   f. Write the add_label action:
        echo '{"action":"add_label","issue":NNN,"label":"bug-report"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
        echo "ACTION: labeled #NNN as bug-report — <reason>" >> "$RESULT_FILE"

   Do NOT also add the backlog label — bug-report is a separate triage
   track that feeds into reproduction automation.

For each issue, choose ONE action and write to result file:

ACTION (substantial — promote, close duplicate, add acceptance criteria):
  echo "ACTION: promoted #NNN to backlog — <reason>" >> "$RESULT_FILE"
  echo "ACTION: closed #NNN as duplicate of #OLDER" >> "$RESULT_FILE"

Body enrichment on promotion (CRITICAL — prevents quality-gate bounce):
  When promoting ANY issue to backlog, you MUST enrich the issue body so
  it passes the quality gate (step 8) on the next gardener run. Before
  writing the add_label manifest action:

  a. Check whether the body already contains ``## Acceptance criteria``
     (with at least one ``- [ ]`` checkbox) and ``## Affected files``
     (with at least one file path). If both are present, skip to (d).

  b. If ``## Affected files`` is missing, infer from the body — look for
     file paths (e.g. ``lib/agent-session.sh:266``), function names,
     script names, or directory references. Use the AGENTS.md directory
     layout to resolve ambiguous mentions (e.g. "gardener" →
     ``gardener/gardener-run.sh``, "dev-poll" → ``dev/dev-poll.sh``).
     Format as a bulleted list under a ``## Affected files`` heading.

  c. If ``## Acceptance criteria`` is missing, derive ``- [ ]`` checkboxes
     from the problem description — each a verifiable condition the fix
     must satisfy.

  d. Construct the full new body = original body text + appended missing
     sections. Write an edit_body action BEFORE the add_label action:
       echo '{"action":"edit_body","issue":NNN,"body":"<full new body>"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"

  e. Write the add_label action:
       echo '{"action":"add_label","issue":NNN,"label":"backlog"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"

  This ensures promoted issues already have the required sections when
  the next gardener run's quality gate inspects them.

DUST (trivial — single-line edit, rename, comment, style, whitespace):
  echo 'DUST: {"issue": NNN, "group": "<file-or-subsystem>", "title": "...", "reason": "..."}' >> "$RESULT_FILE"
  Group by file or subsystem (e.g. "gardener", "lib/env.sh", "dev-poll").
  Do NOT close dust issues — the dust-bundling step auto-bundles groups
  of 3+ into one backlog issue.

VAULT (needs human decision or external resource):
  File a vault procurement item using vault_request():
    source "$(dirname "$0")/../lib/vault.sh"
    TOML_CONTENT="# Vault action: <action_id>
context = \"<description of what decision/resource is needed>\"
unblocks = [\"#NNN\"]

[execution]
# Commands to run after approval
"
    PR_NUM=$(vault_request "<action_id>" "$TOML_CONTENT")
    echo "VAULT: filed PR #${PR_NUM} for #NNN — <reason>" >> "$RESULT_FILE"

CLEAN (only if truly nothing to do):
  echo 'CLEAN' >> "$RESULT_FILE"

Dust vs ore rules:
  Dust: comment fix, variable rename, whitespace/formatting, single-line edit, trivial cleanup with no behavior change
  Ore:  multi-file changes, behavioral fixes, architectural improvements, security/correctness issues

Sibling dependency rule (CRITICAL):
  Issues from the same PR review or code audit are SIBLINGS — independent work items.
  NEVER add bidirectional ## Dependencies between siblings (creates deadlocks).
  Use ## Related for cross-references: "## Related\n- #NNN (sibling)"

6. Quality gate — backlog label enforcement:
   For each open issue labeled 'backlog', verify it has the required
   sections for dev-agent pickup:
   a. Acceptance criteria — body must contain at least one checkbox
      (``- [ ]`` or ``- [x]``)
   b. Affected files — body must contain an "Affected files" or
      "## Affected files" section with at least one file path

   If either section is missing:
   a. Write a comment action to the manifest:
        echo '{"action":"comment","issue":NNN,"body":"This issue is missing required sections. Please use the issue templates at `.forgejo/ISSUE_TEMPLATE/` — needs: <missing items>."}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
      Where <missing items> is a comma-separated list of what's absent
      (e.g. "acceptance criteria, affected files" or just "affected files").
   b. Write a remove_label action to the manifest:
        echo '{"action":"remove_label","issue":NNN,"label":"backlog"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
   c. Log to the result file:
        echo "ACTION: stripped backlog from #NNN — missing: <missing items>" >> "$RESULT_FILE"

   Well-structured issues (both sections present) are left untouched —
   they are ready for dev-agent pickup.

8. Bug-report lifecycle — auto-close resolved parent issues:
   For each open issue, check whether it is a parent that was decomposed
   into sub-issues. A parent is identified by having OTHER issues whose
   body contains "Decomposed from #N" where N is the parent's number.

   Algorithm:
   a. From the open issues fetched in step 1, collect all issue numbers.
   b. For each open issue number N, search ALL issues (open AND closed)
      for bodies containing "Decomposed from #N":
        curl -sf -H "Authorization: token $FORGE_TOKEN" \
          "$FORGE_API/issues?state=all&type=issues&limit=50" \
        | jq -r --argjson n N \
          '[.[] | select(.body != null) | select(.body | test("Decomposed from #" + ($n | tostring) + "\\b"))] | length'
      If zero sub-issues found, skip — this is not a decomposed parent.

   c. If sub-issues exist, check whether ALL of them are closed:
        curl -sf -H "Authorization: token $FORGE_TOKEN" \
          "$FORGE_API/issues?state=all&type=issues&limit=50" \
        | jq -r --argjson n N \
          '[.[] | select(.body != null) | select(.body | test("Decomposed from #" + ($n | tostring) + "\\b"))]
           | {total: length, closed: [.[] | select(.state == "closed")] | length}
           | .total == .closed'
      If the result is "false", some sub-issues are still open — skip.

   d. If ALL sub-issues are closed, collect sub-issue numbers and titles:
        SUB_ISSUES=$(curl -sf -H "Authorization: token $FORGE_TOKEN" \
          "$FORGE_API/issues?state=all&type=issues&limit=50" \
        | jq -r --argjson n N \
          '[.[] | select(.body != null) | select(.body | test("Decomposed from #" + ($n | tostring) + "\\b"))]
           | .[] | "- #\(.number) \(.title)"')

   e. Write a comment action listing the resolved sub-issues.
      Use jq to build valid JSON (sub-issue titles may contain quotes/backslashes,
      and SUB_ISSUES is multiline — raw interpolation would break JSONL):
        COMMENT_BODY=$(printf 'All sub-issues have been resolved:\n%s\n\nClosing this parent issue as all decomposed work is complete.' "$SUB_ISSUES")
        jq -n --argjson issue N --arg body "$COMMENT_BODY" \
          '{action:"comment", issue: $issue, body: $body}' \
          >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"

   f. Write a close action:
        jq -n --argjson issue N \
          '{action:"close", issue: $issue, reason: "all sub-issues resolved"}' \
          >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"

   g. Log the action:
        echo "ACTION: closed #N — all sub-issues resolved" >> "$RESULT_FILE"

   Edge cases:
   - Already closed parent: skipped (only open issues are processed)
   - No sub-issues found: skipped (not a decomposed issue)
   - Multi-cause bugs: stays open until ALL sub-issues are closed

Processing order:
  1. Handle PRIORITY_blockers_starving_factory first — promote or resolve
  2. Quality gate — strip backlog from issues missing acceptance criteria or affected files
  3. Bug-report detection — label qualifying issues before other classification
  4. Bug-report lifecycle — close parents whose sub-issues are all resolved
  5. Process tech-debt issues by score (impact/effort)
  6. Classify remaining items as dust or route to vault

Do NOT bundle dust yourself — the dust-bundling step handles accumulation,
dedup, TTL expiry, and bundling into backlog issues.

CRITICAL: If this step fails for any reason, log the failure and move on.
"""
needs = ["preflight"]

# ─────────────────────────────────────────────────────────────────────
# Step 3: dust-bundling — accumulate, expire, and bundle dust items
# ─────────────────────────────────────────────────────────────────────

[[steps]]
id    = "dust-bundling"
title = "Accumulate dust, expire stale entries, and bundle groups"
description = """
Process DUST items emitted during grooming. This step maintains the
persistent dust accumulator at $PROJECT_REPO_ROOT/gardener/dust.jsonl.

IMPORTANT: Use $PROJECT_REPO_ROOT/gardener/dust.jsonl (the main repo
checkout), NOT the worktree copy — the worktree is destroyed after the
session, so changes there would be lost.

1. Collect DUST JSON lines emitted during grooming (from the result file
   or your notes). Each has: {"issue": NNN, "group": "...", "title": "...", "reason": "..."}

2. Deduplicate: read existing dust.jsonl and skip any issue numbers that
   are already staged:
     DUST_FILE="$PROJECT_REPO_ROOT/gardener/dust.jsonl"
     touch "$DUST_FILE"
     EXISTING=$(jq -r '.issue' "$DUST_FILE" 2>/dev/null | sort -nu || true)
   For each new dust item, check if its issue number is in EXISTING.
   Add new entries with a timestamp:
     echo '{"issue":NNN,"group":"...","title":"...","reason":"...","ts":"YYYY-MM-DDTHH:MM:SSZ"}' >> "$DUST_FILE"

3. Expire stale entries (30-day TTL):
     CUTOFF=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ)
     jq -c --arg c "$CUTOFF" 'select(.ts >= $c)' "$DUST_FILE" > "${DUST_FILE}.tmp" && mv "${DUST_FILE}.tmp" "$DUST_FILE"

4. Bundle groups with 3+ distinct issues:
   a. Count distinct issues per group:
        jq -r '[.group, (.issue | tostring)] | join("\\t")' "$DUST_FILE" | sort -u | cut -f1 | sort | uniq -c | sort -rn
   b. For each group with count >= 3:
      - Collect issue details and distinct issue numbers for the group
      - Write a create_issue action to the manifest:
          echo '{"action":"create_issue","title":"fix: bundled dust cleanup — GROUP","body":"...","labels":["backlog"]}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
      - Write comment + close actions for each source issue:
          echo '{"action":"comment","issue":NNN,"body":"Bundled into dust cleanup issue for GROUP"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
          echo '{"action":"close","issue":NNN,"reason":"bundled into dust cleanup for GROUP"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
      - Remove bundled items from dust.jsonl:
          jq -c --arg g "GROUP" 'select(.group != $g)' "$DUST_FILE" > "${DUST_FILE}.tmp" && mv "${DUST_FILE}.tmp" "$DUST_FILE"

5. If no DUST items were emitted and no groups are ripe, skip this step.

CRITICAL: If this step fails, log the failure and move on.
"""
needs = ["grooming"]

# ─────────────────────────────────────────────────────────────────────
# Step 4: agents-update — AGENTS.md watermark staleness + size enforcement
# ─────────────────────────────────────────────────────────────────────

[[steps]]
id    = "agents-update"
title = "Check AGENTS.md watermarks, discover structural changes, update stale files"
description = """
Maintain all AGENTS.md files by detecting structural drift since the last
review. Uses git history as the source of truth — not vibes.

## Part A: Discover what changed

1. Read the HEAD SHA from preflight:
     HEAD_SHA=$(cat /tmp/gardener-head-sha)

2. Find all AGENTS.md files:
     find "$PROJECT_REPO_ROOT" -name "AGENTS.md" -not -path "*/.git/*"

3. For each file, read the watermark from line 1:
     <!-- last-reviewed: <sha> -->
   If no watermark exists, treat the file as fully stale (review everything).

4. Check for changes since the watermark:
     git log --oneline <watermark>..HEAD -- <directory>
   If zero changes, the file is current — skip it.

5. For each stale file, run a STRUCTURAL DIFF — this is the core of the step:

   a. FILE INVENTORY: list files at watermark vs HEAD for this directory:
        git ls-tree -r --name-only <watermark> -- <directory>
        git ls-tree -r --name-only HEAD -- <directory>
      Diff the two lists. Categorize:
        - NEW files: in HEAD but not in watermark
        - DELETED files: in watermark but not in HEAD
        - Check AGENTS.md layout section: does it list each current file?
          Files present in the directory but absent from the layout = GAPS.
          Files listed in the layout but missing from the directory = LIES.

   b. REFERENCE VALIDATION: extract every file path, function name, and
      shell variable referenced in the AGENTS.md. For each:
        - File paths: verify the file exists (ls or git ls-tree HEAD)
        - Function names: grep for the definition in the codebase
        - Script names: verify they exist where claimed
      Any reference that fails validation is a LIE — flag it for correction.

   c. SEMANTIC CHANGES: for files that existed at both watermark and HEAD,
      check if they changed meaningfully:
        git diff <watermark>..HEAD -- <directory>/*.sh <directory>/*.py <directory>/*.toml
      Look for: new exported functions, removed functions, renamed files,
      changed CLI flags, new environment variables, new configuration.
      Ignore: internal refactors, comment changes, formatting.

6. For each stale file, apply corrections:
   - Add NEW files to the layout section
   - Remove DELETED files from the layout section
   - Fix every LIE found in reference validation
   - Add notes about significant SEMANTIC CHANGES
   - Set the watermark to HEAD_SHA
   - Conventions: document architecture and WHY, not implementation details

## Part B: Size limit enforcement

After all updates, count lines in the root AGENTS.md:
     wc -l < "$PROJECT_REPO_ROOT/AGENTS.md"

If it exceeds 200 lines, split verbose sections into per-directory files
using progressive disclosure:

7. Identify sections that can be extracted to per-directory files.
   Keep the root AGENTS.md as a table of contents — brief overview,
   directory layout, summary tables with links to detail files.

8. For each extracted section, create a `{dir}/AGENTS.md` with:
   - Line 1: watermark <!-- last-reviewed: <HEAD_SHA> -->
   - The full section content, preserving structure and detail

9. Replace extracted sections in root with concise summaries + links.

10. Verify root is under 200 lines. If still over, extract more.

## Staging

11. Stage all AGENTS.md files created or changed:
      find . -name "AGENTS.md" -not -path "./.git/*" -exec git add {} +

12. If no files need updating AND root is under 200 lines, skip entirely.

CRITICAL: If this step fails for any reason, log the failure and move on.
Do NOT let an AGENTS.md failure prevent the commit-and-pr step.
"""
needs = ["dust-bundling"]

# ─────────────────────────────────────────────────────────────────────
# Step 5: commit-and-pr — single commit with all file changes
# ─────────────────────────────────────────────────────────────────────

[[steps]]
id    = "commit-and-pr"
title = "One commit with all file changes, push, create PR, monitor to merge"
description = """
Collect all file changes from this run (AGENTS.md updates + pending-actions
manifest) into a single commit. All repo mutation API calls (comments, closures,
label changes, issue creation) are deferred to the manifest — the orchestrator
executes them after the PR merges.

1. Convert the JSONL manifest to a JSON array:
     cd "$PROJECT_REPO_ROOT"
     JSONL_FILE="$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
     JSON_FILE="$PROJECT_REPO_ROOT/gardener/pending-actions.json"
     if [ -s "$JSONL_FILE" ]; then
       jq -s '.' "$JSONL_FILE" > "$JSON_FILE"
     else
       echo '[]' > "$JSON_FILE"
     fi
     rm -f "$JSONL_FILE"

2. Check for staged or unstaged changes:
     git status --porcelain

   If there are no file changes (no AGENTS.md updates AND manifest is empty []),
   skip to step 4 — no commit, no PR needed.

3. If there are changes:
   a. Create a branch:
        BRANCH="chore/gardener-$(date -u +%Y%m%d-%H%M)"
        git checkout -B "$BRANCH"
   b. Stage all modified AGENTS.md files:
        find . -name "AGENTS.md" -not -path "./.git/*" -exec git add {} +
   c. Stage the pending-actions manifest:
        git add gardener/pending-actions.json
   d. Also stage any other files the gardener modified (if any):
        git add -u
   e. Commit:
        git commit -m "chore: gardener housekeeping $(date -u +%Y-%m-%d)"
   f. Push:
        git push -u origin "$BRANCH"
   g. Create a PR:
        PR_RESPONSE=$(curl -sf -X POST \
          -H "Authorization: token $FORGE_TOKEN" \
          -H "Content-Type: application/json" \
          "$FORGE_API/pulls" \
          -d '{"title":"chore: gardener housekeeping",
               "head":"'"$BRANCH"'","base":"'"$PRIMARY_BRANCH"'",
               "body":"Automated gardener housekeeping — AGENTS.md updates + pending actions manifest.\\n\\nReview `gardener/pending-actions.json` for proposed grooming actions (label changes, closures, comments). These execute after merge."}')
        PR_NUMBER=$(echo "$PR_RESPONSE" | jq -r '.number')
   h. Save PR number for orchestrator tracking:
        echo "$PR_NUMBER" > /tmp/gardener-pr-${PROJECT_NAME}.txt
   i. The orchestrator handles CI/review via pr_walk_to_merge.
      The gardener stays alive to inject CI results and review feedback
      as they come in, then executes the pending-actions manifest after merge.

4. If no file changes existed (step 2 found nothing):
     # Nothing to commit — the gardener has no work to do this run.
     exit 0

5. If PR creation fails, log the error and exit.
"""
needs = ["agents-update"]
