---
name: gdd-turn-closeout
description: "Portable mirror of the gdd-turn-closeout Stop hook (D-11). Closes the events.jsonl gap at turn-end and surfaces a stage-completion or paused-mid-task nudge. Tail-called by orchestrator skills ({{command_prefix}}next, {{command_prefix}}design, {{command_prefix}}verify) at exit on the 13 non-Claude runtimes that lack a Stop hook surface. Idempotent, non-blocking, ≤10ms typical."
argument-hint: "(none - reads .design/STATE.md and .design/telemetry/events.jsonl from cwd)"
tools: Read, Bash
---

# gdd-turn-closeout

## Role

You are a deterministic **closeout** skill. You close the per-turn telemetry gap on runtimes that don't expose a Stop event (codex, gemini, and 11 others). You are a code-level mirror of `hooks/gdd-turn-closeout.js` (D-10): same conditions, same idempotence, same emitted event shape. The only difference: the JS hook emits the nudge as `additionalContext` via the harness; this skill prints the nudge directly to the user. A turn is "complete" when the verify command exits 0, the `<done>` criterion is observable, and the diff stays within the declared scope - any deviation is logged for the run summary.

**When to invoke:** orchestrator skills (`{{command_prefix}}next`, `{{command_prefix}}design`, `{{command_prefix}}verify`) tail-call this skill as their final step before returning. Adoption is incremental - each orchestrator can wire the tail-call independently; the skill exists as a stable, callable surface today.

## Invocation Contract

- **Input**: none. Operates on `.design/STATE.md` and `.design/telemetry/events.jsonl` in cwd.
- **Output**: at most one printed line - the nudge - or silent return.
- **Latency budget**: ≤10ms typical. Read **only** STATE.md and the tail of events.jsonl.
- **Idempotence**: if the most recent event is already a `turn_end` for the current `(stage, task_progress)` tuple, skip append but still print nudge.
- **Non-blocking**: any I/O failure → silent return. This skill never gates the user.

## Algorithm

Execute in order; stop at the first early-return.

### Step 1 - Read STATE.md

Read `.design/STATE.md`. Missing or unreadable → **return silently** (no print, no append).

### Step 2 - Parse `<position>` block

Lightweight-parse only `<position>…</position>` (regex `/<position>([\s\S]*?)<\/position>/` then per-line `key: value`). Extract `stage`, `status`, `task_progress`. Do not invoke the full STATE parser.

If `status != "in_progress"` → **return silently**. Pipeline is initialized / completed / blocked - no turn-end gap to close.

### Step 3 - Tail the last event line

Read **only the last 8 KiB** of `.design/telemetry/events.jsonl`. Treat as "stale by definition":

- File missing.
- File empty.
- Last line fails JSON parse.
- Last line's `timestamp` is missing or unparseable.

Otherwise compute `now - last_event.timestamp`. Gap < 60 seconds → user is actively mid-turn → **return silently** (next real event closes the gap naturally). Bash one-liner for the tail: `tail -n 1 .design/telemetry/events.jsonl 2>/dev/null`.

### Step 4 - Idempotence check, then append

If last event is already `{type:"turn_end", stage:<same>, payload:{task_progress:<same>}}` for the exact `(stage, task_progress)` from Step 2: **skip append** but proceed to Step 5.

Otherwise append one JSONL line to `.design/telemetry/events.jsonl`:

```json
{"type":"turn_end","timestamp":"<ISO 8601 now>","sessionId":"<session-id-or-'turn-closeout'>","stage":"<stage>","payload":{"task_progress":"<N/M>"},"_meta":{"source":"gdd-turn-closeout-skill"}}
```

Create `.design/telemetry/` if missing. Append is a single `appendFile`-equivalent call (writer assumes append-atomicity per Plan 20-06).

### Step 5 - Print the nudge

Match `task_progress` against `^(\d+)/(\d+)$`:

- **Numerator equals denominator and denominator > 0** (e.g. `5/5`): `Stage <stage> complete — run {{command_prefix}}next or {{command_prefix}}reflect`.
- **Otherwise** (mid-task, e.g. `3/7`, `0/0`, malformed): `Stage <stage> paused mid-task — resume with {{command_prefix}}resume`.

One line exactly. No commentary - the nudge is the user-facing surface.

## Failure Modes

| Condition | Behavior |
|-----------|----------|
| STATE.md missing/unreadable | Silent return |
| `<position>` absent or malformed | Silent return |
| `status != "in_progress"` | Silent return |
| `events.jsonl` missing | Treat as stale → fall through to append + nudge |
| Last event unparseable | Treat as stale → fall through |
| Last event < 60s old | Silent return |
| Append fails (permission, disk full) | Print nudge anyway; do not surface I/O error |
| Any uncaught throw | Silent return |

## Equivalence with the JS hook

This skill and `hooks/gdd-turn-closeout.js` MUST stay code-level equivalent: same four early-return branches, same 60-second staleness threshold, same idempotence guard (`type=turn_end, stage, payload.task_progress`), same event shape (only `_meta.source` differs: `gdd-turn-closeout` vs `gdd-turn-closeout-skill` so reflector telemetry can distinguish hook-driven vs skill-driven). Same nudge wording for both N/N and mid-task cases. Change one → change the other in the same plan. Plan 25-09's `tests/turn-closeout-hook.test.cjs` covers the JS hook.

## Non-Goals

- **Not a state writer.** Never edits STATE.md. The events.jsonl append is the only side effect.
- **Not a stage transition.** `turn_end` is within-stage, not a state-machine move; downstream tools ignore it for transition gating.
- **Not a Stop-event harness.** Cross-runtime Stop-event support at the harness level is out-of-scope for Phase 25 (CONTEXT.md OOS).

## Integration Point

Canonical tail-call sites per D-11: `{{command_prefix}}next`, `{{command_prefix}}design`, `{{command_prefix}}verify`. Each orchestrator's final step before returning to the user invokes `gdd-turn-closeout`. Tail-call wiring intentionally not part of v1.25 (Plan 25-04 ships only the callable surface).
