183 lines
11 KiB
Markdown
183 lines
11 KiB
Markdown
> 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 <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).
|
|||
|
|
|
|||
|
|
```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` —
|
|||
|
|
`工单 API(见 TERMINOLOGY)search(q: "#N", type: "pulls", state: "open")` (API source)
|
|||
|
|
or `gitea_pull__list` — 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.
|
|||
|
|
|
|||
|
|
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: <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 <BASE>/session/<LAUNCHER_SID>/prompt_async?directory=<DIR>
|
|||
|
|
(编排目录未知时可用全局路由 POST <BASE>/prompt_async,body 携 sessionID,[org-internal #4307])
|
|||
|
|
body: {"agent":"builder","model":{...},"parts":[{"type":"text","text":"<report>"}]}
|
|||
|
|
<report>: [#N] [<node-or-slug>] status=done|blocked|handoff branch=<ref> [pr=#<PR>] verify=<changed+typecheck> risk=<high|low> summary=<one line>
|
|||
|
|
约束:worker 只推分支不开 PR(PR 由编排串行开);交付推送前 test:changed+typecheck 须绿。若再遇配额/用量上限错误,立即回报 status=blocked reason=quota-exhausted summary=<provider 与重置提示>(branch=<ref> 必带)后停止,勿自行换模型重试。
|
|||
|
|
回报失败(非 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"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
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]).
|