* 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>
197 lines
7.8 KiB
Markdown
197 lines
7.8 KiB
Markdown
# 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
|
||
|
||
```bash
|
||
_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):
|
||
|
||
```bash
|
||
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:
|
||
|
||
```yaml
|
||
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:
|
||
|
||
```text
|
||
📚 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:
|
||
|
||
```text
|
||
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:
|
||
```markdown
|
||
### {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`:
|
||
```yaml
|
||
- 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`:
|
||
```yaml
|
||
- 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:
|
||
```text
|
||
[graduation: deferred all remaining clusters to next transition]
|
||
```
|
||
Then proceed directly to Step 7.
|
||
|
||
---
|
||
|
||
## Step 7: Completion Report
|
||
|
||
After processing all clusters, print:
|
||
|
||
```text
|
||
Graduation complete: {promoted} promoted, {deferred} deferred, {dismissed} dismissed.
|
||
```
|
||
|
||
If no clusters qualified (all filtered by backlog or threshold), print:
|
||
```text
|
||
[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
|