This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git


The following commit(s) were added to refs/heads/main by this push:
     new ceeeb0b  feat(pr-management-triage): add session-history gist 
persistence (Step 6b) (#343)
ceeeb0b is described below

commit ceeeb0b43d8968872593997c3b21c8fdb3c22048
Author: Jarek Potiuk <[email protected]>
AuthorDate: Wed May 27 22:46:56 2026 +0200

    feat(pr-management-triage): add session-history gist persistence (Step 6b) 
(#343)
    
    Add an optional Step 6b at session end that proposes appending the
    session's action counts, override notes, and pre-filter breakdown to a
    long-lived **private** GitHub gist on the maintainer's account. The
    gist URL persists in a gitignored adopter-repo file
    (`.apache-steward.session-state.json`) so subsequent runs of the skill
    update the same gist instead of creating a new one each time.
    
    Why this exists. A single session shows what happened this morning;
    a multi-session history shows which rules consistently fire with no
    override and which heuristics need recalibration. That second view
    is the input signal for promoting "human-confirmed" actions to
    "automated" in future framework revisions.
    
    What is added
    - `SKILL.md` — new Step 6b after the on-screen summary, soft-fails
      to a one-line notice when the `gh` token lacks `gist` scope or
      when `no-history` / `dry-run` is active.
    - `session-history.md` — content schema, create-vs-update logic,
      local state-file location, maintainer-confirmation flow, failure
      modes, privacy guarantees, and what the file is NOT.
    - `prerequisites.md` — non-blocking `gist` scope check with the
      exact `gh auth refresh -s gist` remediation command.
    - `projects/_template/pr-management-config.md` — new
      `session_history_gist` workflow choice (default `enabled`).
    - New `no-history` selector documented in SKILL.md parameters.
    
    Privacy. The gist is secret-by-default; the skill refuses to write
    to a gist that resolves as public. The schema records action verbs
    and override reasons typed by the maintainer — never PR comment
    bodies, diffs, author emails, or credential material.
---
 .claude/skills/pr-management-triage/SKILL.md       |  42 ++-
 .../skills/pr-management-triage/prerequisites.md   |  18 ++
 .../skills/pr-management-triage/session-history.md | 296 +++++++++++++++++++++
 projects/_template/pr-management-config.md         |   1 +
 4 files changed, 355 insertions(+), 2 deletions(-)

diff --git a/.claude/skills/pr-management-triage/SKILL.md 
b/.claude/skills/pr-management-triage/SKILL.md
index 125faf2..de8a464 100644
--- a/.claude/skills/pr-management-triage/SKILL.md
+++ b/.claude/skills/pr-management-triage/SKILL.md
@@ -572,8 +572,45 @@ On exit, print a one-screen summary:
   page)
 - total wall-clock time and PRs-per-minute velocity
 
-The summary is for the maintainer's records — this skill never
-writes a session log to disk beyond the scratch cache.
+The on-screen summary is for the maintainer's quick read at
+session end.
+
+### Step 6b — Propose session-history gist update
+
+After the on-screen summary, the skill proposes appending the
+session to a long-lived **private GitHub gist** so the
+maintainer can review automation calibration across many
+sessions. The proposal step is **always confirm-before-mutate**
+— gist content is published under the maintainer's account.
+
+The gist captures:
+
+- per-action PR counts and the PR numbers (so the maintainer can
+  re-open any individual decision later),
+- per-rule "rule-fired" vs "user-overrode" counts (the input
+  signal for which actions can be safely automated further),
+- per-PR notes when the maintainer overrode the proposed action
+  (the reason matters more than the override itself),
+- stale-sweep counts and any deferrals.
+
+See [`session-history.md`](session-history.md) for the gist
+content schema, the create-vs-update logic, the local
+state-file location, and the maintainer-confirmation flow.
+
+The local state file
+(`.apache-steward.session-state.json` at the adopter repo root,
+gitignored) is the persistence anchor — it stores the gist URL
+across sessions so subsequent runs of the skill update the same
+gist rather than creating a new one each time.
+
+This step is a no-op when:
+
+- `gh auth status` reports a token without `gist` scope (the
+  skill prints a one-line warning pointing at
+  [`prerequisites.md`](prerequisites.md) and continues),
+- the maintainer passes `--no-history` (see
+  [Parameters](#parameters-the-user-may-pass)),
+- or `dry-run` is active.
 
 ---
 
@@ -611,6 +648,7 @@ writes a session log to disk beyond the scratch cache.
 | `dry-run` | classify and propose but refuse to execute any action |
 | `clear-cache` | invalidate the scratch cache before running |
 | `stale` | run stale sweeps only, skip Steps 2–5 for non-stale PRs |
+| `no-history` | skip Step 6b (don't propose the session-history gist update); 
the on-screen summary still prints. See 
[`session-history.md`](session-history.md). |
 
 When in doubt about the selector, ask the maintainer
 *before* fetching — a one-line clarification is cheaper than a
diff --git a/.claude/skills/pr-management-triage/prerequisites.md 
b/.claude/skills/pr-management-triage/prerequisites.md
index 4b3706d..b91c949 100644
--- a/.claude/skills/pr-management-triage/prerequisites.md
+++ b/.claude/skills/pr-management-triage/prerequisites.md
@@ -36,6 +36,24 @@ do not try to parse tokens from the environment. A maintainer
 running through `gh` also gets the TTY login prompt handled for
 them when the token expires.
 
+### `gist` scope (non-blocking)
+
+[Step 6b](SKILL.md#step-6b--propose-session-history-gist-update)
+(session-history gist persistence) needs the `gist` scope on
+the `gh` token. If the scope is missing, the rest of the skill
+runs unchanged; only Step 6b is skipped with a one-line notice.
+
+To add the scope:
+
+```bash
+gh auth refresh -s gist
+```
+
+`gist` is a non-blocking prerequisite by design — first-time
+adopters can run the skill end-to-end without ever creating a
+history gist, and only opt in once they want the cross-session
+calibration view.
+
 ---
 
 ## 2. Viewer has collaborator access to `<repo>` (blocking for mutations)
diff --git a/.claude/skills/pr-management-triage/session-history.md 
b/.claude/skills/pr-management-triage/session-history.md
new file mode 100644
index 0000000..ccfe243
--- /dev/null
+++ b/.claude/skills/pr-management-triage/session-history.md
@@ -0,0 +1,296 @@
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/licenses/LICENSE-2.0 -->
+
+# Session-history gist
+
+Step 6b of [`SKILL.md`](SKILL.md) proposes appending each
+triage session to a long-lived, **private** GitHub gist scoped
+to one adopter repo. This file is the contract for that gist:
+location of the local state anchor, content schema, create vs.
+update logic, and the maintainer-confirmation flow.
+
+Why this exists:
+
+- A single triage session shows what happened *this morning*.
+  A multi-session history shows *which rules consistently fire
+  with no override*, *which rules the maintainer routinely
+  rewrites*, and *which heuristics need recalibration*.
+- That second view is the input signal for which actions can be
+  promoted from "human-confirmed" to "automated" in future
+  framework revisions. Without persistence, every session's
+  data is lost the moment the skill exits.
+- The gist is the simplest viable backend: free, private,
+  attributed to the maintainer's GitHub account, accessible
+  from any machine the maintainer logs into.
+
+---
+
+## Local state file
+
+**Path.** `<adopter-repo-root>/.apache-steward.session-state.json`.
+
+**Status.** Gitignored. The adopter repo's snapshot mechanism
+already gitignores `.apache-steward.local.lock` next to the
+session-state file; the same `.gitignore` entry covers both.
+
+**Schema.**
+
+```json
+{
+  "pr-management-triage": {
+    "history_gist_id":  "c419315f2ac318f74a3e63134757723a",
+    "history_gist_url": 
"https://gist.github.com/<viewer>/c419315f2ac318f74a3e63134757723a",
+    "history_filename": "triage-history.md"
+  }
+}
+```
+
+The top-level key is per-skill; other skills (e.g.
+`pr-management-stats`) may add their own keys later. The
+schema is deliberately additive — never remove or rename
+fields, only add.
+
+**Reads.** Step 6b reads the file at session entry. Missing
+file or missing `pr-management-triage.history_gist_id` is
+treated as "first run" — the create path fires.
+
+**Writes.** Step 6b writes the file once, after a successful
+gist create (the update path doesn't change the URL). Atomic
+write: write to a temp file alongside, `fsync`, rename. Never
+write a partial JSON file — a malformed state file blocks
+future runs.
+
+---
+
+## Required `gh` scope
+
+The token used by `gh` must carry the `gist` OAuth scope. Step
+6b's pre-flight check:
+
+```bash
+scopes=$(gh auth status 2>&1 | sed -n 's/.*Token scopes: //p' | head -1)
+case "$scopes" in
+  *gist*) ;;
+  *) echo "history-gist: gh token lacks 'gist' scope — skipping. Re-run \`gh 
auth refresh -s gist\` to enable." >&2; exit 0 ;;
+esac
+```
+
+A missing scope is **not** an error — Step 6b is opt-in and
+soft-fails to a one-line notice so the rest of the session
+summary still prints. Document the re-auth command on the
+notice line so the maintainer can fix it before the next run.
+
+See [`prerequisites.md#gh-scopes`](prerequisites.md) for the
+canonical scope list.
+
+---
+
+## Gist filename and structure
+
+**Filename.** `triage-history.md` (Markdown, single file).
+
+**Description.** `PR Triage History — <repo> — maintained by <viewer>`.
+
+**Visibility.** Always created with the default-secret flag (no
+`--public`). The skill MUST refuse to write to a public gist
+even if pointed at one — if the gist URL in the local state
+file resolves to a public gist via
+`gh api gists/<id> --jq .public`, the skill aborts Step 6b with
+a one-line notice and asks the maintainer to delete the public
+gist and re-run.
+
+**Content layout.** Reverse chronological — newest session at
+the top. Each session is a `## <YYYY-MM-DD> — <repo>` heading,
+collapsible by GitHub's gist renderer.
+
+Session block template (Markdown):
+
+```markdown
+## <YYYY-MM-DD HH:MM UTC> — <repo>
+
+**Maintainer:** @<viewer>  ·  **Agent:** <agent-name-and-version>  ·  
**Session length:** Nm
+
+### Action counts
+
+| Action | Count | PR numbers |
+|---|---|---|
+| `mark-ready` | N | #..., #..., ... |
+| `approve-workflow` | N | #..., #..., ... |
+| `draft` | N | #... |
+| `comment` | N | #... |
+| `rebase` | N | #... |
+| `rerun` | N | #... |
+| `ping` | N | #... |
+| `request-author-confirmation` | N | #... |
+| `close` (deterministic_flag) | N | #... |
+| `close` (stale-sweep 1a) | N | #... |
+| `close` (stale-sweep 1b) | N | #... |
+| `draft` (stale-sweep 2) | N | #... |
+| `draft` (stale-sweep 3) | N | #... |
+| `strip-ready-label` (stale-sweep 4a) | N | #... |
+| `close` (stale-sweep 4b) | N | #... |
+| `ping` (stale-sweep 5) | N | #... |
+| `promote-bot-draft` (Step 0.5) | N | #... |
+| `flag-suspicious` | N | #... |
+
+### Pre-filter skips
+
+| Filter | Count |
+|---|---|
+| F1 collaborator/member/owner | N |
+| F2 bot | N |
+| F3 active draft (≤14d) | N |
+| F4 already-ready-no-regression | N |
+| F5a recent maintainer comment | N |
+| F5b unanswered maintainer-to-maintainer ping | N |
+| F6 maintainer co-drafted | N |
+
+### Decision-rule signal
+
+One row per decision-table rule that fired this session. The
+"overrides" column counts cases where the maintainer rejected
+the proposed action and picked something else (skip, different
+verb, batch deferral). High override rates are the calibration
+signal.
+
+| Rule | Fired | Auto-confirmed | Maintainer overrode | Override notes |
+|---|---|---|---|---|
+| Row 1 (`pending_workflow_approval`) | N | N | N | One-line per override |
+| Row 2 (`stale_copilot_review`) | N | N | N | |
+| Row 9 (CONFLICTING → draft) | N | N | N | |
+| ... | | | | |
+
+### Per-PR override notes
+
+PRs where the maintainer picked a non-default action are listed
+here so the framework-maintenance loop can review them:
+
+- #<NN> — rule said `<X>`, maintainer chose `<Y>` because <reason>.
+- ...
+
+### Deferrals
+
+- Stale-sweep 1b candidates left untouched: N (member-authored)
+- Stale-sweep 2 candidates left untouched: N (member-authored)
+- Row 22 (`unclassified` — rollup not settled): N PRs to retry next sweep
+
+---
+```
+
+The schema is intentionally flat-table heavy and free of
+free-form prose between sections. The next session's append
+operation is a simple "insert this block at line 1 of the gist
+body, after the H1 if present". A more nested layout would
+require parsing on every update; the flat-table schema does
+not.
+
+---
+
+## Create vs. update logic
+
+```text
+state := read_local_state_file()
+if state.history_gist_id is null:
+    # First-run path — create
+    body := render_session_block() + intro_header()
+    confirm with maintainer (show first 50 lines + URL it will live at)
+    on confirm:
+      url := gh gist create --desc "<desc>" - < /tmp/body.md
+      write_local_state_file(url, gist_id_from(url))
+else:
+    # Steady-state path — update
+    existing := gh gist view <gist_id> --filename triage-history.md
+    new_body := render_session_block() + "\n\n---\n\n" + existing
+    confirm with maintainer (show first 50 lines of *new* block + gist URL)
+    on confirm:
+      gh gist edit <gist_id> --filename triage-history.md - < /tmp/new_body.md
+```
+
+**First-run preview.** The maintainer sees:
+
+```text
+About to CREATE a private gist on your account:
+  Description: PR Triage History — <repo> — maintained by @<viewer>
+  Filename:    triage-history.md
+  Visibility:  secret (default; not public)
+  Length:      <N> lines
+  Local state will be written to: .apache-steward.session-state.json
+
+First 50 lines:
+─────────────────────────────────────────────────────
+<...>
+─────────────────────────────────────────────────────
+
+[Y] create  [N] skip Step 6b for this run  [E] edit before posting
+```
+
+**Steady-state preview.** Show the new block being **prepended**:
+
+```text
+About to UPDATE existing gist:
+  URL:      <history_gist_url>
+  Length:   appending +<N> lines (new total: <M>)
+
+First 50 lines of new section:
+─────────────────────────────────────────────────────
+<...>
+─────────────────────────────────────────────────────
+
+[Y] update  [N] skip Step 6b for this run  [E] edit before posting
+```
+
+`[E]` opens `$EDITOR` (or `gh gist edit`'s `--editor` mode) on
+the rendered Markdown and resumes after save.
+
+---
+
+## Failure modes and recovery
+
+| Failure | Detection | Recovery |
+|---|---|---|
+| Local state references a deleted gist | `gh api gists/<id>` returns 404 | 
Treat as first-run; warn the maintainer that the previous gist is gone; create 
a new one on confirm; overwrite the local state. |
+| Local state references a *public* gist | `gh api gists/<id> --jq .public` 
returns `true` | Refuse to write. Print a notice instructing the maintainer to 
delete the public gist (`gh gist delete <id>`) and re-run. |
+| Local state JSON is malformed | `json.JSONDecodeError` on read | Refuse to 
write. Print the parse error + path; the maintainer must fix or delete the 
file. Never silently rewrite a corrupt file — silent loss is worse than a stop. 
|
+| `gh gist create` fails (network, scope) | Non-zero exit | Print the `gh` 
stderr verbatim. The on-screen Step 6 summary is unaffected; the session is 
still recorded for the maintainer's own copy. |
+| `gh gist edit` race (someone else edited the gist between view and edit) | 
`gh gist edit` 4xx with conflict | The skill is the only writer to its own gist 
by contract, so this should not happen. If it does, refuse to write, surface 
the conflict, and let the maintainer reconcile manually. |
+| Two skill instances running concurrently (e.g. two worktrees) | The second 
instance's `view` sees the first's update; the prepend is correct, no conflict 
| Native to the prepend pattern — no special handling needed. |
+
+---
+
+## Privacy
+
+The gist is private by default. It still ends up under the
+maintainer's GitHub account and is **not** anonymous to the
+maintainer's collaborators with access to the link.
+
+The skill MUST NOT include:
+
+- PR comment bodies verbatim (only the action verb + reason
+  string from `classify-and-act.md`),
+- diff snippets (those live only in the per-session scratch
+  cache),
+- author email addresses or any field outside the PR / repo
+  metadata that drove the classification,
+- the `gh` token, any cookie, or any credential material.
+
+Override reasons typed by the maintainer DO go into the gist
+(the maintainer is the author of that text). The skill should
+not paraphrase or expand them.
+
+---
+
+## What this file is NOT
+
+- A long-term analytics warehouse. Use
+  [`pr-management-stats`](../pr-management-stats/SKILL.md) for
+  cross-session metrics over the live PR queue. Session-history
+  is *append-only* and human-readable; it is not designed to be
+  parsed back into structured data.
+- A replacement for git history on the framework repo. Concrete
+  rule changes still go via PR to
+  [`apache/airflow-steward`](https://github.com/apache/airflow-steward).
+  The session-history gist is the **input** to those PRs; the
+  framework repo's diff is the **output**.
+- A substitute for the on-screen summary. Step 6 always prints
+  the maintainer-facing summary; Step 6b is the persistence
+  layer on top.
diff --git a/projects/_template/pr-management-config.md 
b/projects/_template/pr-management-config.md
index b77354c..7a36307 100644
--- a/projects/_template/pr-management-config.md
+++ b/projects/_template/pr-management-config.md
@@ -79,3 +79,4 @@ default to use the standard variant.
 | Key | Default | Notes |
 |---|---|---|
 | `confirmation_handback_mode` | `reviewer-ping` | 
`request-author-confirmation` action's "If yes" branch. `reviewer-ping`: the 
author marks threads resolved and `@`-pings the reviewer for a final look + 
label. `maintainer-sweep`: the author replies with a short `yes / ready` and 
the next triage sweep promotes the PR to the maintainer review queue. Pick 
`maintainer-sweep` if your project runs a regular maintainer triage cadence and 
prefers a lightweight contributor confirmation over a re [...]
+| `session_history_gist` | `enabled` | [Step 
6b](../../.claude/skills/pr-management-triage/SKILL.md#step-6b--propose-session-history-gist-update)
 — propose appending each session to a private GitHub gist on the maintainer's 
account. Set to `disabled` to skip Step 6b unconditionally for this project 
(overrides the per-invocation `no-history` flag). The local state file at 
`.apache-steward.session-state.json` is read regardless so an existing gist 
remains discoverable. See [`session-histor [...]

Reply via email to