Files
octopus-workflow/core/skills/headless-session-ops/reference/ticket-recipe.md
T

11 KiB
Raw Blame History

Extracted from headless-session-ops/SKILL.md (Launch a session for a specific ticket (#N)) — moved verbatim 2026-08-26, ticket [org-internal #3480].

Launch a session for a specific ticket (#N)

The common case: an agent (or cron/CI) needs to spin up a fresh main session to work a tracked issue. Compose the seed prompt from the issue itself so the new session starts with real context, and launch it on the current session's model.

  1. Owner check (MANDATORY pre-step, [org-internal #1803]). Before creating anything, verify no other live session already owns this ticket or code area — duplicate ownership is how [org-internal #1744]/[org-internal #1753] collided. The check spans four data sources; any live claim → ABORT the launch and report the conflict.

    Mechanized ([org-internal #3667]): bash script/session-conflicts.sh <N> runs the session-title scan (a), the branch scan (d), and the open-PR check (c) in one read-only pass (exit 4 = conflict, --json for a machine summary); the assignee/claim check (b) is claim.sh's compare-and-swap at claim time. The manual recipes below remain the fallback and the normative definition (core/rules/session-scope-guard.md).

    a. Session-title scan (live session check). The listing endpoint returns at most limit (default 100) sessions — on busy repos (300+ live sessions in one directory) that silently truncates and the scan misses owners ([org-internal #3190]). Non-range listing responses carry X-Total-Count (full filter population, untruncated) and X-Has-More: true (only when the page is truncated). Scan procedure: fetch page 1, and when X-Has-More: true is present re-fetch once with limit=$TOTAL — treat any failure to obtain the full population as "more owners may exist" (ABORT or widen the scan — never assume the first page is the whole population).

    # N must be the digits-only issue number (e.g. N=1803) — never interpolate
    # raw issue text here; the regex below assumes digits.
    [[ "$N" =~ ^[0-9]+$ ]] || { echo "N must be digits only"; exit 1; }
    HDR=$(mktemp)
    BATCH=$(curl -sD "$HDR" "$BASE/session?directory=$DIR")
    TOTAL=$(awk -F': ' 'tolower($1)=="x-total-count"{print $2}' "$HDR" | tr -d '\r')
    if [[ "$TOTAL" =~ ^[0-9]+$ ]] && awk -F': ' 'tolower($1)=="x-has-more"{print $2}' "$HDR" | grep -q true; then
      # truncated page + known population — refetch the full set in one request
      BATCH=$(curl -s "$BASE/session?limit=$TOTAL&directory=$DIR")
    elif awk -F': ' 'tolower($1)=="x-has-more"{print $2}' "$HDR" | grep -q true; then
      # truncated page but no usable X-Total-Count (older server) — the
      # population is unknown; do NOT re-fetch with the same default limit
      # (it would silently re-truncate). Escalate instead.
      echo "ERROR: session listing truncated but X-Total-Count unavailable — cannot establish full owner population" >&2
      rm -f "$HDR"; exit 1
    fi
    rm -f "$HDR"
    echo "scanned $(echo "$BATCH" | jq 'length') of ${TOTAL:-?} sessions in $DIR" >&2
    echo "$BATCH" | jq -r --arg n "$N" '.[] | select(.title | test("#" + $n + "([^0-9]|$)")) | "\(.id)\t\(.title)\t\(.time.updated)"'
    
    • Any hit whose time.updated is recent (session still active) → ABORT the launch and report the conflict (issue comment naming the owning session id). Do not launch a second session for the same ticket.

    b. Assignee / claim check ([org-internal #2297]). Read the issue via 工单 API(见 TERMINOLOGYget(owner, repo, index: N) (or GET /api/v1/repos/$OWNER/$REPO/issues/$N):

    • If assignee is set and is not the launching agent → ABORT and report (the ticket is already claimed).
    • If a claim comment names a different branch / session idABORT and coordinate on the issue before proceeding.

    c. Open-PR check. List open PRs referencing #NGET <gitea-base-url>/api/v1/repos/issues/search?q="#N"&type=pulls&state=open (API source) — and ABORT if an open PR already covers the ticket.

    d. Remote branch check ([org-internal #2297]). A local branch is invisible to other sessions — check the remote too: git ls-remote origin 'workflow/*' and scan for a branch tail covering #N. Also check the repo side locally: git worktree list + git branch --list 'workflow/*' for a branch / worktree already covering #N.

    • A stale hit (session idle for hours / clearly abandoned) → do NOT hand-post a takeover comment (retired admin workaround — unauditable); dead-claim takeover is mechanized ([org-internal #3668]): bash script/claim.sh takeover <ticket> <branch> --session-id <id>. It enforces the evidence gate (branch ABSENT on origin AND (session 404 OR comment at least CLAIM_TAKEOVER_STALE_DAYS old)) and exits 10 when the evidence does not hold — a valid claim is never superseded; an idle-but-alive session is NOT provably dead, coordinate on the issue instead (exit 4 = owned/conflict, in claim.sh and session-conflicts.sh alike). Evidence rules: core/rules/session-scope-guard.md §"Dead-claim takeover" (claim.sh header is normative). The runtime counterpart of this check is core/rules/session-scope-guard.md ("One task, one owner"), injected into every session's prompt.
    • Backend hard guard ([org-internal #1989]): POST /session hard-rejects duplicate-ticket session creation with HTTP 400. The backend guard is on by default ([org-internal #2350]); set enabled: false in config to opt out. It fails open ONLY on defect paths (DB / config errors), never as a configured disable. The front-end soft check above is now backed by this backend hard check for defense in depth.
    • Claim-first ([org-internal #2297]). Claiming a ticket is one atomic 3-step action: (1) set the issue assignee to the working account, (2) post a claim comment naming the workflow branch and session id, (3) push the workflow branch to remote (git push -u origin workflow/...). The claim is valid ONLY when all three steps complete — an incomplete claim is NOT a claim. Do NOT launch the session until all three steps are done (see core/rules/session-scope-guard.md §"Claim-first"). If a collision HAS already happened, follow rules/ownership-collision-runbook (wiki, L2 on-demand).
    • Provisioning after claim ([org-internal #3642]) is ONE command — claim + worktree + Tier-1 runs scaffold: bash script/claim-provision.sh <N> workflow/<branch> (runs claim.sh with exit codes passed through, attaches the session worktree to the claimed branch via session-worktree.sh create <slug> --branch <branch>, then scaffolds <runs-root>/{slug}/ via <harness-package>/script/runs-init.ts with kind-aware exemptions; prints one JSON summary; idempotent re-runs). Pitfall #10's pre-built worktree is exactly what it produces — put the returned worktree path in the claim comment + seed prompt.
  2. Read the issue. Use the REST API (gitea-rest skill; gitea-mcp is retired):

    curl -s "$GITEA/api/v1/repos/$OWNER/$REPO/issues/$N" | jq '{title, body}'
    

    or 工单 API(见 TERMINOLOGYget(owner, repo, index: N).

  3. Compose the seed prompt from the issue body. The seed MUST, at minimum:

    • restate the goal in one sentence;
    • list the constraints and acceptance criteria;
    • cite every wiki artifact path / related issue referenced in the body (e.g. {epic-slug}/dag, {epic-slug}/dag-nodes/{node-id}, [org-internal #1691]);
    • tell the new agent to follow the right pipeline skill for the work type (implement / bugfix / design / …). Write it to a file (handles newlines):
    cat > seed.txt <<'EOF'
    Work issue #N: <one-line title>.
    Goal: <…>.
    Constraints: <…>.
    Acceptance: <…>.
    Artifacts: <wiki paths from the issue body>.
    Follow the `implement` skill (Mode: bugfix) for this.
    

Session scope guard (mandatory): if a test fails and it was NOT caused by your change — classify (pre-existing/flaky), file a BF/FT issue per core/rules/session-scope-guard.md, and continue this task. Do NOT fix unrelated failing tests in place.

完成回报协议 (mandatory, [org-internal #2374])

完成、阻塞、或移交时,向编排会话回报: POST /session/<LAUNCHER_SID>/prompt_async?directory=

(编排目录未知时可用全局路由 POST /prompt_asyncbody 携 sessionID[org-internal #4307] body: {"agent":"builder","model":{...},"parts":[{"type":"text","text":""}]} : [#N] [] status=done|blocked|handoff branch= [pr=#] verify=<changed+typecheck> risk=<high|low> summary= 约束:worker 只推分支不开 PR(PR 由编排串行开);交付推送前 test:changed+typecheck 须绿。若再遇配额/用量上限错误,立即回报 status=blocked reason=quota-exhausted summary=<provider 与重置提示>branch= 必带)后停止,勿自行换模型重试。 回报失败(非 2xx / 连接拒绝)→ 在本 issue(或父 Epic)发同内容评论兜底。 决策边界([org-internal #2378]):遇方向性决策点 → status=blocked 回报,勿调 question。 EOF


3. **Determine the known-good model** (reuse the current session's — see the
"Reuse the current session's model" section).

4. **Run the 3-step flow** with `title:"#N — <short>"` and the file-built
payload. Read the model from the launcher session's own record — this also
applies the `model.id → modelID` remap from the "Reuse" section:

```sh
MODEL=$(curl -s "$BASE/session/$LAUNCHER_SID?directory=$DIR" \
  | jq -c '.model | {providerID, modelID: .id}')
SID=$(curl -s -X POST "$BASE/session?directory=$DIR" \
  -H 'content-type: application/json' \
  -d "{\"title\":\"#$N — <short>\",\"agent\":\"builder\"}" | jq -r .id)
PAYLOAD=$(jq -n --argjson m "$MODEL" --rawfile p seed.txt \
  '{agent:"builder",model:$m,parts:[{type:"text",text:$p}]}')
curl -s -X POST "$BASE/session/$SID/prompt_async?directory=$DIR" \
  -H 'content-type: application/json' -d "$PAYLOAD"
  1. Poll Step 3 until an assistant message appears — bounded tail poll (&limit=1), never the no-limit full-transcript form. Record $SID somewhere durable (issue comment, CI log) so the session is traceable to the ticket.

  2. Add the completion-report block to the seed prompt (see next section) so the worker reports back when it finishes or blocks — without it, this orchestrator has no push channel and must poll forever ([org-internal #2374]).