# Example workflow for users: copy it to .github/workflows/loki-issue-to-pr.yml in your own repository.
# It is not run by this repository CI.
# Hand Loki an issue, get a pull request.
#
# Label an issue `loki` (or comment `/loki` as a repository owner, member or
# collaborator) and this opens a PR with an evidence receipt. Copy this file
# into .github/workflows/ of any repository.
#
# REQUIRED: set ANTHROPIC_API_KEY in repository secrets. Without it the run
# cannot call a model and fails fast rather than burning minutes.
#
# Rule of Two: issue text is untrusted, so the agent that reads it never holds
# a write token. The work is split in two jobs on two fresh runners:
#   agent    reads the issue and runs Loki with a read-only token. Its only
#            output is an artifact: a patch and a PR body.
#   publish  holds the write token and runs no agent. It applies the patch as
#            data, pushes a branch and opens the PR.
# A patch that touches .github/workflows/ cannot be pushed with this token
# (GitHub requires the `workflows` permission), so the publish job fails
# visibly rather than letting an agent rewrite CI.
name: Loki - Issue to PR

on:
  issues:
    types: [labeled]
  issue_comment:
    types: [created]
  workflow_dispatch:
    inputs:
      issue:
        description: 'Issue number to resolve'
        required: true

# Serialize per issue. Two agents racing on one issue would open two PRs from
# two branches and neither would see the other's work.
concurrency:
  group: loki-issue-${{ github.event.issue.number || inputs.issue }}
  cancel-in-progress: false

# Nothing by default; each job asks for exactly what it needs.
permissions: {}

jobs:
  agent:
    # Gate on an explicit signal so the agent never fires on unrelated
    # activity. A label needs triage access; a `/loki` comment must come from
    # someone with write-level association, not any GitHub user.
    if: >-
      (github.event_name == 'issues' && github.event.label.name == 'loki') ||
      (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/loki') &&
       contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) ||
      github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    timeout-minutes: 45
    permissions:
      contents: read   # checkout
      issues: read     # Loki reads the issue
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # the agent branches and diffs; a shallow clone breaks both
          persist-credentials: false   # leave no token in .git/config for the agent

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Fail fast without a model key
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
            echo "::error::ANTHROPIC_API_KEY is not set in repository secrets. Add it under Settings > Secrets and variables > Actions."
            exit 1
          fi

      - name: Install Loki Mode
        run: npm install -g loki-mode

      - name: Resolve the issue (no write access)
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          # Read-only in this job (contents: read, issues: read). Loki uses it
          # to fetch the issue and withholds it from the agent session.
          GH_TOKEN: ${{ github.token }}
          ISSUE: ${{ github.event.issue.number || inputs.issue }}
          # engine10 reads no cost or iteration cap; its only cap is wall time.
          LOKI_E10_CAP_S: '2400'
          LOKI_BUDGET_LIMIT: '10.00'      # not read by engine10; kept for the legacy path
          LOKI_MAX_ITERATIONS: '20'       # not read by engine10; kept for the legacy path
          # Never push or open a PR from here; the publish job does that.
          LOKI_DELEGATE_PR: '0'
        run: |
          set -uo pipefail
          case "$ISSUE" in
            ''|*[!0-9]*) echo "::error::Issue number must be digits, got '$ISSUE'."; exit 1 ;;
          esac

          # git identity: Loki commits the session's work on its own branch.
          git config user.name  "${GITHUB_ACTOR:-loki-mode}"
          git config user.email "${GITHUB_ACTOR:-loki-mode}@users.noreply.github.com"

          RC=0
          # Loki 10 issue entry. --no-pr is required: engine10 ignores
          # LOKI_DELEGATE_PR, and this job's read-only token cannot push, open a
          # PR or comment (supervisor.ts main: noPr drops opts.pr and opts.comment).
          # The run commits on branch loki/<runId> checked out in this very
          # checkout (stages/intake.ts ensureBranch), so HEAD carries the change.
          loki "${GITHUB_REPOSITORY}#${ISSUE}" --no-pr || RC=$?

          OUT="$RUNNER_TEMP/loki-out"
          mkdir -p "$OUT"
          # The deliverable is what the session committed on top of the commit
          # this run started from. .loki/ is run state, never part of the change.
          git diff --binary "$GITHUB_SHA" HEAD -- . ':(exclude).loki' > "$OUT/loki.patch"

          # PR body: the Loki 10 receipt this run sealed under .loki/runs/e10-*/.
          # Rendered from the run's own receipt.md; the digest comes from receipt.json.
          RUN_DIR="$(ls -d .loki/runs/e10-* 2>/dev/null | sort | tail -n 1)"
          {
            echo "Resolves #${ISSUE}."
            echo ""
            if [ -n "$RUN_DIR" ] && [ -f "$RUN_DIR/receipt.md" ]; then
              head -c 20000 "$RUN_DIR/receipt.md"
              cp "$RUN_DIR/receipt.json" "$OUT/receipt.json" 2>/dev/null || true
              SHA="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("receipt_sha256",""))' "$RUN_DIR/receipt.json" 2>/dev/null || true)"
              echo ""
              echo "Receipt digest (sha256): ${SHA:-unavailable}"
              echo "Check it: download the loki-patch artifact of this run and run \`loki verify receipt.json\`."
            else
              echo "Evidence Receipt: unavailable for this run (no receipt under .loki/runs/)."
            fi
          } > "$OUT/pr-body.md"

          if [ -s "$OUT/loki.patch" ]; then
            echo "Loki produced a change; the publish job opens the pull request."
          else
            echo "::warning::Loki produced no committed change, so no pull request will be opened."
          fi
          exit "$RC"

      - name: Hand the patch to the publish job
        uses: actions/upload-artifact@v4
        with:
          name: loki-patch
          path: ${{ runner.temp }}/loki-out/
          if-no-files-found: error
          retention-days: 7

  publish:
    # Runs only after the agent job succeeded. Everything it reads from that
    # job is attacker-controllable data: no job outputs, no `${{ }}` of agent
    # values in a script, nothing from the artifact is executed.
    needs: agent
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      contents: write        # push the branch
      pull-requests: write   # open the PR
      issues: write          # report back on the issue
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.sha }}   # the commit the agent started from
          persist-credentials: false

      - uses: actions/download-artifact@v4
        with:
          name: loki-patch
          path: ${{ runner.temp }}/loki-out

      - name: Open the pull request
        env:
          GH_TOKEN: ${{ github.token }}
          ISSUE: ${{ github.event.issue.number || inputs.issue }}
          BASE: ${{ github.ref_name }}
        run: |
          set -euo pipefail
          case "$ISSUE" in
            ''|*[!0-9]*) echo "::error::Issue number must be digits, got '$ISSUE'."; exit 1 ;;
          esac

          PATCH="$RUNNER_TEMP/loki-out/loki.patch"
          BODY="$RUNNER_TEMP/loki-out/pr-body.md"
          if [ ! -s "$PATCH" ]; then
            echo "The agent produced no change; nothing to open."
            exit 0
          fi
          [ -s "$BODY" ] || echo "Resolves #${ISSUE}." > "$BODY"

          BRANCH="loki/issue-${ISSUE}-${GITHUB_RUN_ID}"
          git config user.name  "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git switch -c "$BRANCH"
          git apply --index "$PATCH"
          git commit -q -m "Loki: resolve #${ISSUE}"
          gh auth setup-git
          git push origin "HEAD:refs/heads/${BRANCH}"

          PR_URL="$(gh pr create --base "$BASE" --head "$BRANCH" \
            --title "Loki: resolve #${ISSUE}" --body-file "$BODY")"
          echo "Pull request: $PR_URL"

          # Build the comment in a file: a blank line inside an inline YAML
          # string terminates the block scalar and breaks the workflow parse.
          {
            echo "Loki opened a pull request: $PR_URL"
            echo ""
            echo "The PR body carries an evidence receipt stating what was verified,"
            echo "what was skipped, and what is degraded. Check it rather than taking"
            echo "the result on trust."
          } > "$RUNNER_TEMP/loki-issue-comment.md"
          gh issue comment "$ISSUE" --body-file "$RUNNER_TEMP/loki-issue-comment.md" || true
