11 KiB
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.
-
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,--jsonfor a machine summary); the assignee/claim check (b) isclaim.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 carryX-Total-Count(full filter population, untruncated) andX-Has-More: true(only when the page is truncated). Scan procedure: fetch page 1, and whenX-Has-More: trueis present re-fetch once withlimit=$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.updatedis 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)(orGET /api/v1/repos/$OWNER/$REPO/issues/$N):- If
assigneeis 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 <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 iscore/rules/session-scope-guard.md("One task, one owner"), injected into every session's prompt. - Backend hard guard ([org-internal #1989]):
POST /sessionhard-rejects duplicate-ticket session creation with HTTP 400. The backend guard is on by default ([org-internal #2350]); setenabled: falsein 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 (seecore/rules/session-scope-guard.md§"Claim-first"). If a collision HAS already happened, followrules/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 viasession-worktree.sh create <slug> --branch <branch>, then scaffolds<runs-root>/{slug}/via<harness-package>/script/runs-init.tswith 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.
- Any hit whose
-
Read the issue. Use the REST API (
gitea-restskill;gitea-mcpis retired):curl -s "$GITEA/api/v1/repos/$OWNER/$REPO/issues/$N" | jq '{title, body}'or
工单 API(见 TERMINOLOGY)get(owner, repo, index: N). -
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_async,body 携 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"
-
Poll Step 3 until an
assistantmessage appears — bounded tail poll (&limit=1), never the no-limitfull-transcript form. Record$SIDsomewhere durable (issue comment, CI log) so the session is traceable to the ticket. -
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]).