Files
msd-core/get-shit-done/workflows/graduation.md
Tom Boucher 79002a00cb chore(#518): rename npm package + bin to @opengsd/gsd-core (#519)
* chore: rename npm package + bin to @opengsd/gsd-core (functional)

- package.json: name @opengsd/get-shit-done-redux → @opengsd/gsd-core,
  bin key get-shit-done-redux → gsd-core, repository/homepage/bugs URLs
- package-lock.json: regenerated (npm install --package-lock-only)
- tests/**, scripts/**, bin/**, .github/**, agents/**, commands/**,
  get-shit-done/bin/**, get-shit-done/workflows/**:
  applied the 4-rule replacement (scoped npm ref, GitHub repo path,
  bin/clone invocations) per #505 single-source refactor

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: sweep live references to @opengsd/gsd-core

Update all live documentation (README.md + translations, docs/**,
CONTRIBUTING.md, VERSIONING.md, SECURITY.md, CONTEXT.md,
docs/CANARY.md) to reflect the renamed package and repository.

Rules applied:
- @opengsd/get-shit-done-redux → @opengsd/gsd-core (scoped npm name)
- open-gsd/get-shit-done-redux → open-gsd/gsd-core (GitHub repo)
- GSD-redux/get-shit-done-redux → open-gsd/gsd-core (stale badge org)
- bare bin/clone refs → gsd-core

CHANGELOG.md, docs/adr/**, docs/RELEASE-*.md, docs/research/**,
and .changeset/** are preserved byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: add negative lookbehind to slash-command regex in bug-2954 test

The extractSlashReferences regex matched /gsd-core inside npm package
URLs (@opengsd/gsd-core), producing a false /gsd:core command reference.
Adding a negative lookbehind (?<![a-z]) excludes matches preceded by a
letter, so only standalone /gsd-<cmd> and /gsd:<cmd> tokens are found.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#518): add changeset for package rename

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#518): update package-identity expectations to the renamed coordinates

The rebase regenerated the seam to @opengsd/gsd-core (bin gsd-core, repo
open-gsd/gsd-core). The #498 seam tests assert deriveIdentity against the REAL
package.json, so their expected literals must follow the rename. The drift-lint
unit test is left as-is — its SEAM is a self-consistent fixture and its
stale-literal detection cases would shift if altered; the live-repo scan in it
already passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 17:25:02 -04:00

7.8 KiB
Raw Blame History

graduation.md — LEARNINGS.md Cross-Phase Graduation Helper

Invoked by: transition.md step graduation_scan. Never invoked directly by users.

This workflow clusters recurring items across the last N phases' LEARNINGS.md files and surfaces promotion candidates to the developer via HITL. No item is promoted without explicit developer approval.


Configuration

Read from project config (config.json):

Key Default Description
features.graduation true Master on/off switch. false skips silently.
features.graduation_window 5 How many prior phases to scan
features.graduation_threshold 3 Minimum cluster size to surface

Step 1: Guard Checks

_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/get-shit-done/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/get-shit-done/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/get-shit-done/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/get-shit-done/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/get-shit-done/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi
GRADUATION_ENABLED=$(gsd_run query config-get features.graduation 2>/dev/null || echo "true")
GRADUATION_WINDOW=$(gsd_run query config-get features.graduation_window 2>/dev/null || echo "5")
GRADUATION_THRESHOLD=$(gsd_run query config-get features.graduation_threshold 2>/dev/null || echo "3")

Skip silently (print nothing) if:

  • features.graduation is false
  • Fewer than graduation_threshold completed prior phases exist (not enough data)

Skip silently (print nothing) if total items across all LEARNINGS.md files in the window is fewer than 5.


Step 2: Collect LEARNINGS.md Files

Find LEARNINGS.md files from the last N completed phases (excluding the phase currently completing):

find .planning/phases -name "*-LEARNINGS.md" | sort | tail -n "$GRADUATION_WINDOW"

For each file found:

  1. Parse the four category sections: ## Decisions, ## Lessons, ## Patterns, ## Surprises
  2. Extract each ### Item Title + body as a single item record: { category, title, body, source_phase, source_file }
  3. Skip items that already contain **Graduated:** — they have been promoted and must not re-surface

Step 3: Cluster by Lexical Similarity

For each category independently, cluster items using Jaccard similarity on tokenized title+body:

Tokenization: lowercase, strip punctuation, split on whitespace, remove stop words (a, an, the, is, was, in, on, at, to, for, of, and, or, but, with, from, that, this, by, as).

Jaccard similarity: |A ∩ B| / |A ∪ B| where A and B are token sets. Two items are in the same cluster if similarity ≥ 0.25.

Clustering algorithm: single-pass greedy — process items in phase order; add to the first cluster whose centroid (union of all cluster tokens) has similarity ≥ 0.25 with the new item; otherwise start a new cluster.

Cluster size filter: only surface clusters with distinct source phases ≥ graduation_threshold (not just total items — same item repeated in one phase still counts as 1 distinct phase).


Step 4: Check graduation_backlog in STATE.md

Read .planning/STATE.md graduation_backlog section (if present). Format:

graduation_backlog:
  - cluster_id: "{sha256-of-cluster-title}"
    status: "dismissed"   # or "deferred"
    deferred_until: "phase-N"  # only for deferred entries
    cluster_title: "{representative title}"

Skip any cluster whose cluster_id matches a dismissed entry.

Skip any cluster whose cluster_id matches a deferred entry where deferred_until phase has not yet completed.


Step 5: Surface Promotion Candidates

For each qualifying cluster, determine the suggested target file:

Category Suggested Target
decisions PROJECT.md — append under ## Validated Decisions (create section if absent)
patterns PATTERNS.md — append under the appropriate category section (create file if absent)
lessons PROJECT.md — append under ## Invariants (create section if absent)
surprises Flag for human review — if genuinely surprising 3+ times, something structural is wrong

Print the graduation report:

📚 Graduation scan across phases {M}–{N}:

  HIGH RECURRENCE ({K}/{WINDOW} phases)
  ├─ Cluster: "{representative title}"
  ├─ Category: {category}
  ├─ Sources: {list of NN-LEARNINGS filenames}
  └─ Suggested target: {target file} § {section}

  [repeat for each qualifying cluster, ordered HIGH→LOW recurrence]

For each cluster above, choose an action:
  P = Promote now   D = Defer (re-surface next transition)   X = Dismiss (never re-surface)   A = Defer all remaining

Step 6: HITL — Process Each Cluster

For each cluster (in order from Step 5), ask the developer:

Cluster: "{title}" [{category}, {K} phases] → {target}
Action [P/D/X/A]:

Use AskUserQuestion (or equivalent HITL primitive for the current runtime). If TEXT_MODE is true, display the cluster question as plain text and accept typed input. Accept single-character input: P, D, X, A (case-insensitive).

On P (Promote now):

  1. Read the target file (or create it with a standard header if absent)
  2. Append the cluster entry under the suggested section:
    ### {Cluster representative title}
    {Merged body — combine unique sentences across cluster items}
    
    **Sources:** Phase {A}, Phase {B}, Phase {C}
    **Promoted:** {ISO_DATE}
    
  3. For each source LEARNINGS.md item in the cluster, append **Graduated:** {target-file}:{ISO_DATE} after its last existing field
  4. Commit both the target file and all annotated LEARNINGS.md files in a single atomic commit: docs(learnings): graduate "{cluster title}" to {target-file}

On D (Defer):

Write to .planning/STATE.md under graduation_backlog:

- cluster_id: "{sha256}"
  status: "deferred"
  deferred_until: "phase-{NEXT_PHASE_NUMBER}"
  cluster_title: "{title}"

On X (Dismiss):

Write to .planning/STATE.md under graduation_backlog:

- cluster_id: "{sha256}"
  status: "dismissed"
  cluster_title: "{title}"

On A (Defer all):

Defer the current cluster (same as D) and skip all remaining clusters for this run, deferring each to the next transition. Print:

[graduation: deferred all remaining clusters to next transition]

Then proceed directly to Step 7.


Step 7: Completion Report

After processing all clusters, print:

Graduation complete: {promoted} promoted, {deferred} deferred, {dismissed} dismissed.

If no clusters qualified (all filtered by backlog or threshold), print:

[graduation: no qualifying clusters in phases {M}–{N}]

First-Run Behaviour

On the first transition after upgrading to a version that includes this workflow, all extant LEARNINGS.md files may produce a large batch of candidates at once. A [Defer all] shorthand is available: if the developer enters A at any cluster prompt, all remaining clusters for this run are deferred to the next transition.


No-Op Conditions (silent skip)

  • features.graduation = false
  • Fewer than graduation_threshold prior phases with LEARNINGS.md
  • Total items < 5 across the window
  • All qualifying clusters are in graduation_backlog as dismissed