# Ticket identity & sessions

2build's ticket identity used to live entirely on the git branch:
`bbs ticket env` regex-matched `feat/<ticket>_<slug>` at whatever cwd the
shell sat in, and "my ticket" was a function of `git rev-parse
--abbrev-ref HEAD`. That broke anywhere the cwd wasn't a checkout of the
ticket branch (crashed sessions, per-ticket worktrees, fresh shells).

Identity is now carried by **environment variables and a per-ticket
`manifest.yaml`**, with branch derivation as a fallback inside
per-ticket worktrees.

This document is the contract. Anything that disagrees with what's here
is a bug.

---

## The env triple

| Variable | Set by | Read by | Purpose |
|----------|--------|---------|---------|
| `BABYSIT_TICKET` | `bbs ticket ensure` (prints `TICKET=<id>`; the invoking skill stores it and prefixes later commands — exports don't survive per-call shells), `bbs ticket session attach`, user shell rc, test harnesses | Every skill preamble (via `bbs ticket env`'s env-first override) | Names the ticket id. Short-circuits all branch and cwd inference. |
| `BABYSIT_SESSION` | the host agent's own session id (preamble default), `bbs ticket session attach` | preamble session-writer, `bbs autopilot checkpoint` | Names a single agent run so concurrent / crashed sessions can rehydrate. |

Both are optional. None are required for the legacy single-repo
ergonomic path: a developer who `cd`s into a feat-branch checkout and
runs a skill gets identical behavior to today, because env-unset falls
through to branch derivation.

### Legacy `BBS_TICKET` alias

`BBS_TICKET` continues to work as an alias for `BABYSIT_TICKET`
(read-only, no warning) so existing shell rcs keep functioning. If both
are set and disagree, `bbs ticket env` exits non-zero with an explicit
"unset one of them" message — the calling preamble aborts.

---

## Resolution order

`bbs ticket resolve` is the single identity entry point. Three steps,
each short-circuits:

1. **`$BABYSIT_TICKET`** (or `$BBS_TICKET` alias). If set, echo it.
   If both env vars are set and disagree → exit 2 with `REASON: env conflict`.
2. **`manifest.yaml` cwd match.** Walk every
   `tickets/<id>/manifest.yaml` under the **active project home** (see
   bootstrap below). Pick the manifest whose any `repos[].worktree` is a
   parent of `$PWD`. Exactly one match → echo its `ticket`. Multiple
   matches → exit 2 listing them.
3. **Branch regex** (the historical path): run `bbs ticket env`'s
   `^(feat|fix|chore|bug|refactor|hotfix)/<ticket>_…` regex over the
   current branch.

Exit codes:

| Code | Meaning |
|------|---------|
| 0 | Resolved. Ticket id printed on stdout. |
| 1 | No resolution. (Caller decides whether that's a BLOCK.) |
| 2 | Explicit conflict — env disagree, multiple manifests match cwd, schema violation. (Always BLOCK.) |

### `BABYSIT_PROJECT_HOME` bootstrap

The manifest walk needs a project home. `bbs ticket env` derives the slug
from the git remote (falling back to the directory basename), and the
project home is `~/.babysit/projects/$SLUG/`.

---

## `manifest.yaml` schema

One file per ticket, at `tickets/<ticket-id>/manifest.yaml`.
Auto-generated by `bbs ticket ensure`; never authored by hand.

```yaml
# Schema version. New files are always v1; bbs ticket resolve treats
# unknown versions as a BLOCK (loud forward-compat).
version: 1
ticket: bs-abc12345
title: Add /healthz endpoint           # mirrored from index.json for human readers
created_at: 2026-05-01T11:48:59Z
updated_at: 2026-05-01T11:48:59Z

repos:
  - name: <SLUG>          # what bbs ticket env derives from the git remote
    branch: feat/bs-abc12345_add-healthz
    canonical: .          # repo root
    worktree: .           # "." = the shared checkout (trunk tickets) — the
                          # resolver skips it by design, since several trunk
                          # tickets can share one cwd; an absolute
                          # .babysit/worktrees/ path when the safe-cut gate
                          # diverted the cut — that path participates in the
                          # manifest cwd-match rung
    base: main
    pushed: false         # records whether the branch has been pushed
```

`canonical` is display-only (the dashboard reads it); identity resolution
keys off `worktree`. `.` is the special skipped value — a shared-checkout
(trunk) row is not a claim on the cwd — while absolute paths and other
relative paths (resolved against cwd) participate in the manifest
cwd-match rung. Trunk tickets therefore cannot be recovered by the
manifest rung; bare autopilot resume resolves the ticket through the
identity ladder (env → manifest → branch) and the ticket's own
checkpoint.

`manifest.yaml` is **eagerly written** by `bbs ticket ensure` — there is
no `if file_exists` branch in resolve.

### Naming: `manifest.yaml` vs `manifest.md`

`manifest.yaml` is the identity manifest documented here.
`manifest.md` is the **plan-draft decomposition output** — an unrelated markdown
document listing sub-tickets of an oversized plan. They live in the same
ticket directory but are read by disjoint code paths. If you find
yourself confused, the one extension difference disambiguates them.

---

## `manifest.yaml` writes

| Operation | Who | How |
|-----------|-----|-----|
| Create on ticket creation | `bbs ticket ensure` | Atomic mktemp+mv; under existing index-lock |
| Update single repo's branch | `bbs ticket set-branch <ticket> <repo> <branch>` | Atomic mktemp+mv; bumps `updated_at`; under existing index-lock |
| Read | `bbs ticket get-manifest [<ticket>]` | Validates `version: 1`; round-trips byte-identically to a no-op set-branch |
| Remove one | Dashboard delete | Moves one ticket directory to `~/.babysit/trash` |
| Remove all active tickets | `bbs ticket clear --all` | Moves every `projects/*/tickets/*` directory to `~/.babysit/trash`; leaves configs, sessions, analytics, branches, and worktrees untouched |

All writes acquire the existing `_LOCK_PATH` lock used for `index.json`
mutations. Concurrent set-branch calls serialize.

---

## `.babysit/sessions/<uuid>.yaml` schema

One file per active Claude Code session at
`~/.babysit/sessions/<session-uuid>.yaml`. Auto-written by the
`bbs hooks session-writer` plugin hook (SessionStart + PostToolUse(Bash),
throttled to one write/60s; ticket derived from the cwd's worktree dir or
branch since hooks can't see `$BABYSIT_TICKET`), and by the preamble
session-writer block when a skill executes it with `$BABYSIT_SESSION` set;
bumped by `bbs autopilot checkpoint`.

```yaml
version: 1
session_id: 7d4f1c8e-9a3b-4e2f-9b1c-8a5e6d7f2a3b
ticket: bs-abc12345
started_at: 2026-05-01T11:48:59Z
last_seen_at: 2026-05-01T12:14:22Z    # bumped by bbs autopilot checkpoint
pid: 48291
cwd: /Users/long/workspace/lohi/babysit/.babysit/worktrees/bs-abc12345_add-healthz
```

The existing `~/.babysit/sessions/<PPID>` touch-file (preamble line 137)
stays — its only role is the 120-min stale-session sweep, which now
incidentally covers the new `<uuid>.yaml` files (same dir, same mtime
sweep). **No new long-running process.**

### Atomic writes

Session-writer and checkpoint last-seen bump MUST use atomic mktemp+mv,
not in-place edit. The 120-min stale-session sweep is mtime-based;
in-place edit on Linux preserves mtime, so a long-idle session whose
field bumps but whose file mtime doesn't would get swept after 120 min —
invisible active session. mktemp+mv creates a new inode with current
mtime.

### Skip when `$BABYSIT_SESSION` unset

The preamble hook only writes when `$BABYSIT_SESSION` is set. Skill
preambles run in many no-ticket contexts (direct setup or browse checks,
etc.); writing a session file for those would
pollute the session list.

---

## Session CLI

`bbs ticket session list|attach|end` is the user-facing surface for
sessions.

### `bbs ticket session list`

Reads `~/.babysit/sessions/*.yaml` (skips non-yaml — the `<PPID>` touch
files), filters by `mtime > now - 120m` (matches preamble's existing
window). Output is human-readable; not designed for `eval`.

```
SESSION_ID                              TICKET        AGE   CWD
7d4f1c8e-9a3b-4e2f-9b1c-8a5e6d7f2a3b    bs-abc12345   12m   .../bs-abc12345_add-healthz
```

### `bbs ticket session attach <session-id>`

Prints export lines for `eval`. Idempotent — re-running just re-prints
the same export lines (no file changes).

```bash
$ bbs ticket session attach 7d4f1c8e-…
export BABYSIT_TICKET=bs-abc12345
export BABYSIT_SESSION=7d4f1c8e-9a3b-4e2f-9b1c-8a5e6d7f2a3b

$ eval "$(bbs ticket session attach 7d4f1c8e-…)"
```

### `bbs ticket session end <session-id>`

`rm ~/.babysit/sessions/<id>.yaml`. Idempotent.

---

## Conflict semantics

Six explicit BLOCK shapes. Each emits the three-line
`REASON / ATTEMPTED / RECOMMENDATION` contract from
`handoff-contracts.md`.

| Failure | RECOMMENDATION line |
|---------|--------------------|
| `BBS_TICKET` vs `BABYSIT_TICKET` conflict | `unset one of them, e.g. `unset BBS_TICKET` to use the BABYSIT_TICKET value` |
| `manifest.yaml` malformed YAML | `edit <path> by hand, or move the malformed manifest aside to start over` |
| `manifest.yaml` schema version > 1 | `upgrade babysit (`/plugin marketplace update babysit`) or check out an older babysit version` |
| `BABYSIT_TICKET` set, ticket dir absent | `unset BABYSIT_TICKET, or run `bbs ticket session end <BABYSIT_SESSION>`` |
| `bbs ticket session attach <nonexistent uuid>` | ``bbs ticket session list` to see active sessions` |

### Env wins over disagreeing cwd

When `$BABYSIT_TICKET=bs-A` is set but cwd is inside ticket `bs-B`'s
worktree, **env wins** (resolve step #1 short-circuits; step #2 does not
run). This is intentional — env-first is the predictable ladder. To
switch tickets, `unset BABYSIT_TICKET` then re-resolve.

---

## Caller surface

The ladder (`ticket.ResolveLadder` — env → manifest cwd-match → branch) is
the resolver behind every ticket-*inference* path: `bbs ticket env`,
`ensure`, `resolve`, readiness, and the `ticket_*` handlers that derive
the ticket from the checkout resolve through `resolveEnv()`. This is
required because the preamble's
`eval "$(bbs ticket env)"` sets shell `TICKET` only — it neither exports
`BABYSIT_TICKET` nor survives per-call shells — so each command re-resolves
on its own.

- **Skill init** — `bbs ticket ensure --no-branch` prints `TICKET=<id>`;
  the invoking skill stores it in context and prefixes every later
  `bbs ticket` call with `BABYSIT_TICKET=<id>` (an export only holds
  within the same shell invocation). Downstream commands read it via the
  ladder's env-first rung.
- **Explicit-ticket commands** — `get-manifest <ticket>`,
  `set-branch <ticket> …`, `surface compose <ticket>…`, `land <ticket>…`,
  `surface acquire|release --ticket`, `reconcile --ticket|--all` — resolve project scope
  only (`ticket.ResolveProject`): an unrelated manifest ambiguity in the
  cwd must not reject a fully explicit command. The env-conflict abort
  still applies.
- **Project-only commands** — `board` — use
  `identity.Resolve()` directly; they never need ticket inference.
- **Direct CLI** — `bbs ticket resolve` (and `--explain` for debugging),
  user scripts.

The manifest-cwd walk is `O(N tickets)` and runs only when no env identity
exists — the env rung short-circuits first.

---

## Migration

None. Greenfield. Pre-overhaul tickets that lack `manifest.yaml` work
unchanged via the branch-fallback path (resolve step #3). New tickets
get `manifest.yaml` from `bbs ticket ensure`. Rollback is `git revert`;
leftover `manifest.yaml` files are inert.
