Spaces:
Runtime error
A newer version of the Gradio SDK is available: 6.26.0
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}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/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 "/Users/theogengineer/Projects/Multilingual-Absa/.opencode/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="/Users/theogengineer/Projects/Multilingual-Absa/.opencode/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/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; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; 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.graduationisfalse- Fewer than
graduation_thresholdcompleted 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:
- Parse the four category sections:
## Decisions,## Lessons,## Patterns,## Surprises - Extract each
### Item Title+ body as a single item record:{ category, title, body, source_phase, source_file } - 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 question (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):
- Read the target file (or create it with a standard header if absent)
- 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} - For each source LEARNINGS.md item in the cluster, append
**Graduated:** {target-file}:{ISO_DATE}after its last existing field - 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_thresholdprior phases with LEARNINGS.md - Total items < 5 across the window
- All qualifying clusters are in
graduation_backlogas dismissed