> 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**. 0. **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 ` 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). ```sh # 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(见 TERMINOLOGY)get(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 id** → **ABORT** and coordinate on the issue before proceeding. **c. Open-PR check.** List open PRs referencing `#N` — `GET /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 --session-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 workflow/` (runs claim.sh with exit codes passed through, attaches the session worktree to the claimed branch via `session-worktree.sh create --branch `, then scaffolds `/{slug}/` via `/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. 1. **Read the issue.** Use the REST API (`gitea-rest` skill; `gitea-mcp` is retired): ```sh curl -s "$GITEA/api/v1/repos/$OWNER/$REPO/issues/$N" | jq '{title, body}' ``` or `工单 API(见 TERMINOLOGY)get(owner, repo, index: N)`. 2. **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): ```sh cat > seed.txt <<'EOF' Work issue #N: . Goal: <…>. Constraints: <…>. Acceptance: <…>. Artifacts: . 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//prompt_async?directory= (编排目录未知时可用全局路由 POST /prompt_async,body 携 sessionID,[org-internal #4307]) body: {"agent":"builder","model":{...},"parts":[{"type":"text","text":""}]} : [#N] [] status=done|blocked|handoff branch= [pr=#] verify= risk= summary= 约束:worker 只推分支不开 PR(PR 由编排串行开);交付推送前 test:changed+typecheck 须绿。若再遇配额/用量上限错误,立即回报 status=blocked reason=quota-exhausted summary=(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 — "` 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 — \",\"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" ``` 5. **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. 6. **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]).