Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "idstack",
"version": "3.2.0.0",
"version": "3.3.0.0",
"description": "Evidence-based instructional design pipeline. 11 skills backed by peer-reviewed research across 11 domains. Analyze, design, build, review, and export courses with every recommendation citing its evidence tier.",
"author": {
"name": "Philippos Savvides",
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verify tag matches VERSION, plugin.json, and CHANGELOG
run: |
VER="$(tr -d '[:space:]' < VERSION)"
if [ "v$VER" != "${{ github.ref_name }}" ]; then
echo "Tag ${{ github.ref_name }} does not match VERSION ($VER)" >&2
exit 1
fi
grep -qF "\"version\": \"$VER\"" .claude-plugin/plugin.json || {
echo "plugin.json version does not match VERSION ($VER)" >&2; exit 1; }
grep -qF "## v$VER" CHANGELOG.md || {
echo "CHANGELOG.md has no entry for v$VER" >&2; exit 1; }
- name: Run test suite
run: ./test/smoke-test.sh
- name: Create GitHub Release
run: gh release create "${{ github.ref_name }}" --generate-notes
env:
Expand Down
60 changes: 60 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Test

on:
push:
pull_request:

jobs:
test:
strategy:
fail-fast: false
matrix:
include:
# 3.9 is the oldest interpreter in the field (macOS system python3);
# it is the leg that catches modern-only Python syntax creeping into
# the preamble's embedded scripts.
- os: ubuntu-latest
python-version: '3.9'
- os: ubuntu-latest
python-version: '3.12'
# macOS leg covers BSD grep/sed/awk differences. Pinned to 3.12:
# setup-python has no reliable arm64 3.9 build for macos-latest.
- os: macos-latest
python-version: '3.12'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Smoke test
run: ./test/smoke-test.sh
- name: Integration test
run: ./test/integration-test.sh
- name: Manifest-merge unit tests
run: ./test/test-manifest-merge.sh
- name: Version-classifier unit tests
run: ./test/test-version-classifier.sh
- name: Plugin-status unit tests
run: ./test/test-plugin-status.sh
- name: Preamble embedded-python tests
run: ./test/test-preamble-python.sh
- name: Setup behavioral tests
run: ./test/test-setup.sh

# Confirms the guards above actually fail when their bugs return. Slower
# (copies the repo per mutation), so it runs once rather than per matrix leg.
#
# Pinned to 3.9, the oldest supported interpreter: some defects only exist
# below a given version. PEP 701 (3.12) legalized reusing a quote inside an
# f-string replacement field, so mutating the preamble f-string bug is a
# no-op on 3.12 and the suite would skip that case.
mutation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.9'
- name: Mutation test
run: ./test/mutation-test.sh
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# Changelog

## v3.3.0.0 (2026-08-04)

### Fixed — course memory, pipeline orchestration, re-run detection

A full audit of the toolchain surfaced a set of bugs that broke user-visible behavior, some since their features shipped:

- **Context recovery never ran on Python < 3.12.** The session-memory script embedded in every skill contained an f-string with nested same-type quotes — a SyntaxError on macOS system python3 (3.9) that died silently. Welcome-back messages, quality-score trends, and next-skill suggestions now work, and a new test suite runs the block on Python 3.9 and 3.12 in CI so the class of failure can't ship again.
- **`/idstack:pipeline` invoked child skills with an unnamespaced name** that never resolved in Claude Code. The orchestrator now uses `skill: "idstack:<name>"`, logs its own completion to the timeline, and its status table and prompts use `/idstack:` forms the Codex translation rule can strip.
- **Re-run detection was dead in five skills** (assessment-design, course-builder, course-export, course-import, course-quality-review): their "update or start fresh?" checks looked up manifest section names that don't exist. Corrected to the canonical names; smoke-test now bans the drifted tokens.
- **`$_IDSTACK` resolution missed the marketplace cache** in `learn` and `course-export` — the way most users are installed — so their `bin/` calls pointed at a nonexistent directory. All bash blocks now splice one canonical resolution snippet (new `{{IDSTACK_RESOLVE}}` generator placeholder), re-derived per block because blocks run in separate shells.
- **`bin/idstack-status` blanked the dashboard** when the course name contained an apostrophe (shell text interpolated into Python source). The name now travels via the environment; readiness failures print a message instead of vanishing. `course-import` counts as a pipeline entry for next-step suggestions.
- **`bin/idstack-doctor` could report a broken install as healthy.** Its enabled-check read a fixed 4-line window of `claude plugin list`, so when another plugin was listed right after idstack, the neighbour's `enabled` line was attributed to idstack — a disabled install diagnosed as "installed and enabled". The check is now scoped to idstack's own entry. Doctor also parses the plugin version as JSON rather than by regex, and `--local` no longer scans `$HOME`.
- **`setup`**: `--keep-legacy` is honored in all legacy-removal paths (was 1 of 3); `--local` no longer touches `$HOME/.claude/plugins`; `claude plugin` failures error loudly with manual-recovery steps instead of aborting silently.

### Changed

- course-quality-review, course-export, and learning-objectives now write their manifest sections through `bin/idstack-manifest-merge` (atomic, section-scoped, preserves every other section). needs-analysis and course-import keep the Read-modify-Write path — both write several co-owned sections in one pass, which whole-section merge cannot express — and each now documents why.
- `bin/idstack-migrate --init` creates a canonical manifest with every section at its default. Three skills told the user to run `idstack-migrate` to create one when running standalone; it was a no-op on a missing file, so the merge that followed died with exit 4 and standalone results were silently never persisted. The skeleton comes from running the existing migration chain over a minimal seed, so there is no second definition of "canonical".
- learning-objectives reports gained the required "Top recommendations" section; `[Alignment-1]` is now correctly cited as T5.
- Logic that was duplicated or inlined and therefore untestable now lives in `bin/lib/` and is sourced by its callers: `version-classify.sh` (shared by `setup` and `bin/idstack-doctor`) and `plugin-status.sh` (the `claude plugin list` parser). Their unit tests exercise the shipped code rather than a copy — the version classifier had drifted across three PRs while a mirrored test passed green.

### Infrastructure

The audit found the test suite had never run automatically, and that some of it was not testing what it appeared to test. Both are fixed:

- **CI.** New GitHub Actions `test.yml` runs all seven suites on push and PR (ubuntu + macos, Python 3.9 + 3.12 — 3.9 is the leg that catches the context-recovery class of bug). `release.yml` refuses to publish unless the tag, `VERSION`, `plugin.json`, and `CHANGELOG.md` agree and the smoke test passes.
- **`./setup` is now tested** — 16 behavioral tests covering flag parsing, scope selection, all three legacy-cleanup shapes, and failure handling, run against a repo copy with a fake `$HOME` and a stub `claude`. It previously had no coverage at all while the smoke test spent 14 assertions on landing-page CSS.
- **A mutation suite proves the guards work.** `test/mutation-test.sh` reintroduces each of the 13 defects fixed here into a throwaway copy and asserts the guarding test fails. This is what was missing: the version-classifier suite passed green while testing a local copy of the classifier rather than the shipped code, and `gen-skills` counted a placeholder-less template as neither generated nor failed.
- smoke-test grew from 272 to 371 assertions (version agreement, canonical section names, `/idstack:` namespacing, resolve-snippet lockstep, v1.1 migration, `bash -n` on every script) and prints failure diagnostics instead of a bare FAIL; integration-test proves it leaves the working tree untouched.

## v3.2.0.0 (2026-05-14)

### Fixed — Claude Code install uses the plugin marketplace flow
Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# idstack

> **Status: beta.** Skills work end-to-end and ship behind a versioned plugin (`v2.5.0`), but expect rough edges and breaking changes between minor versions while we shake out the rest of the workflow with real instructional designers. [Tell us what's broken or missing.](https://forms.gle/6LDgDD1M6WWyYvME8)

> **New in v2.5.0:** native support for **OpenAI Codex CLI** alongside Claude Code. Same 11 skills, same evidence base, same `.idstack/` manifest. Run `./setup` and idstack auto-detects which CLIs you have installed. (Gemini CLI on the v2.6 roadmap.)
> **Status: beta.** Skills work end-to-end and ship behind a versioned plugin, but expect rough edges and breaking changes between minor versions while we shake out the rest of the workflow with real instructional designers. See [CHANGELOG.md](CHANGELOG.md) for what's new in the current release. [Tell us what's broken or missing.](https://forms.gle/6LDgDD1M6WWyYvME8)

Decades of research say elaborated feedback improves learning. Cognitive load theory has been replicated for 30 years. Constructive alignment measurably raises grades. The evidence is strong, but most course design tools don't make it easy to apply.

Expand Down
19 changes: 10 additions & 9 deletions TODOS.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# TODOS

## ~~v2.4: Dual-Output Report Contract + Pipeline Aggregator~~ SHIPPED (v2.4.0.0)
Shipped in v2.4.0.0. Every finding-producing skill now writes both
`.idstack/project.json` (system state) and `.idstack/reports/<skill>.md` (human view)
following the canonical observation → evidence → why-it-matters → suggestion structure
in `templates/report-format.md`. `/idstack:pipeline` produces `.idstack/reports/pipeline.md`
— a cross-cutting aggregate over per-skill reports with top recurring issues, evidence
themes, and where to start. `bin/idstack-status` lists every report under
`.idstack/reports/` with `pipeline.md` first. Plus install-hygiene fix: setup actively
removes pre-v2.0.1.0 dispatcher clones, smoke-test catches the regression, new
`bin/idstack-doctor` for diagnostics.
Shipped in v2.4.0.0 with Markdown reports under `.idstack/reports/` (historical —
later releases replaced that path with self-contained HTML reports under
`.idstack/exports/<course-slug>/`, which is the current contract). Every
finding-producing skill writes both `.idstack/project.json` (system state) and a
human-view report following the canonical observation → evidence → why-it-matters
→ suggestion structure in `templates/report-format.md`. `/idstack:pipeline`
produces the cross-cutting aggregate (now `index.html` in the export folder)
with top recurring issues, evidence themes, and where to start. Plus
install-hygiene fix: setup actively removes pre-v2.0.1.0 dispatcher clones,
smoke-test catches the regression, new `bin/idstack-doctor` for diagnostics.

## ~~v1.1: Manifest Versioning~~ SHIPPED (v1.2.0)
Shipped in v1.2.0. `bin/idstack-migrate` handles schema migrations.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.2.0.0
3.3.0.0
48 changes: 30 additions & 18 deletions bin/idstack-doctor
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ set -u
# its own location so the diagnosis works regardless of where the repo sits.
IDSTACK_DIR="$(cd "$(dirname "$0")/.." && pwd -P)"

# Shared legacy-VERSION classifier (also sourced by setup; pinned by
# test/test-version-classifier.sh).
. "$IDSTACK_DIR/bin/lib/version-classify.sh"
# `claude plugin list` parsing (pinned by test/test-plugin-status.sh).
. "$IDSTACK_DIR/bin/lib/plugin-status.sh"

SCOPE="global"
LEGACY_BASE="$HOME/.claude"
# Match --local in any position, not just $1. Robust if other flags get added.
Expand All @@ -42,7 +48,14 @@ echo "Plugin manifests:"
plugin_manifest="$IDSTACK_DIR/.claude-plugin/plugin.json"
market_manifest="$IDSTACK_DIR/.claude-plugin/marketplace.json"
if [ -f "$plugin_manifest" ]; then
version=$(grep -o '"version": *"[^"]*"' "$plugin_manifest" | head -n1 | sed 's/.*"\([^"]*\)"$/\1/')
# Prefer a real JSON parse (also catches a malformed manifest); fall back to
# grep|sed only when python3 is unavailable.
version=""
if command -v python3 >/dev/null 2>&1; then
version=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("version",""))' "$plugin_manifest" 2>/dev/null || true)
else
version=$(grep -o '"version": *"[^"]*"' "$plugin_manifest" | head -n1 | sed 's/.*"\([^"]*\)"$/\1/')
fi
if [ -n "$version" ]; then
echo " plugin.json present (version $version)"
else
Expand Down Expand Up @@ -86,7 +99,10 @@ echo "Claude Code install:"
if command -v claude >/dev/null 2>&1; then
plugin_list=$(claude plugin list 2>/dev/null || true)
if echo "$plugin_list" | grep -q "idstack@idstack"; then
if echo "$plugin_list" | grep -A4 "idstack@idstack" | grep -qi "enabled"; then
# Scoped to idstack's own entry — see bin/lib/plugin-status.sh. A fixed
# -A4 window read a neighboring plugin's status line and called a disabled
# idstack enabled. Pinned by test/test-plugin-status.sh.
if echo "$plugin_list" | plugin_is_enabled "idstack@idstack"; then
echo " idstack@idstack installed and enabled"
else
echo " PROBLEM: idstack@idstack is installed but not enabled."
Expand All @@ -112,14 +128,13 @@ legacy_problem=0

# Vestigial bare symlink from the pre-marketplace setup method. Harmless on
# current Claude Code (ignored), but worth removing so it stops confusing
# diagnostics. Informational, not counted as a problem.
for plugins_base in "$HOME/.claude/plugins" "$(pwd)/.claude/plugins"; do
vestigial="$plugins_base/idstack"
if [ -L "$vestigial" ]; then
echo " NOTE: vestigial symlink at $vestigial (pre-marketplace install method)."
echo " Harmless — ./setup removes it. Or: rm \"$vestigial\""
fi
done
# diagnostics. Informational, not counted as a problem. Scope-aware: --local
# checks only ./.claude/plugins, the default checks only $HOME/.claude/plugins.
vestigial="$LEGACY_BASE/plugins/idstack"
if [ -L "$vestigial" ]; then
echo " NOTE: vestigial symlink at $vestigial (pre-marketplace install method)."
echo " Harmless — ./setup removes it. Or: rm \"$vestigial\""
fi

# Pre-v2.0.1.0 dispatcher install — the failure mode that surfaced as
# "Unknown skill: idstack:course-import" / "Unknown skill: course-import".
Expand All @@ -135,14 +150,11 @@ elif [ -d "$LEGACY_DIR" ]; then
fi
if [ -f "$LEGACY_DIR/VERSION" ]; then
v=$(tr -d '[:space:]' < "$LEGACY_DIR/VERSION")
# Two arms: explicitly skip modern/future versions first, then flag the
# legacy ones. Mirrors setup. Patterns avoid literal dots and single-digit
# ranges so multi-digit components (2.0.10.0, 2.10.0.0, 20.x, 100.0.0,
# 200.0.0) work. Covered by test/test-version-classifier.sh.
case "$v" in
2.0.[1-9]*|2.[1-9]*|[3-9]*|[1-9][0-9]*) ;;
0.*|1.*|2.0.0.*|2.0.0) signature="${signature:+$signature, }VERSION=$v" ;;
esac
# Classified by the shared classifier in bin/lib/version-classify.sh
# (pinned by test/test-version-classifier.sh).
if [ "$(classify_version "$v")" = "legacy" ]; then
signature="${signature:+$signature, }VERSION=$v"
fi
fi
if [ -n "$signature" ]; then
echo " PROBLEM: pre-v2.0.1.0 install at $LEGACY_DIR ($signature)"
Expand Down
Loading
Loading