diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 76422be..ffd0b4d 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -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", diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20132f2..4690099 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7337054 --- /dev/null +++ b/.github/workflows/test.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 36e3c8f..5b1acde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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:"`, 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 diff --git a/README.md b/README.md index 7b68912..d9a6576 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/TODOS.md b/TODOS.md index 87bda9c..526a49d 100644 --- a/TODOS.md +++ b/TODOS.md @@ -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/.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//`, 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. diff --git a/VERSION b/VERSION index 69a1172..ec285fb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.0.0 +3.3.0.0 diff --git a/bin/idstack-doctor b/bin/idstack-doctor index 8525da3..53fc570 100755 --- a/bin/idstack-doctor +++ b/bin/idstack-doctor @@ -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. @@ -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 @@ -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." @@ -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". @@ -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)" diff --git a/bin/idstack-gen-skills b/bin/idstack-gen-skills index 04a97ac..4d1a435 100755 --- a/bin/idstack-gen-skills +++ b/bin/idstack-gen-skills @@ -15,15 +15,19 @@ # allowlist. # all (default): both targets # -# Source of truth is unchanged: skills//SKILL.md.tmpl with {{PREAMBLE}} and -# {{MANIFEST_SCHEMA}} placeholders. The preamble defines the AskUserQuestion / Skill -# / Agent concepts portably so skill bodies don't need per-target text substitution. +# Source of truth is unchanged: skills//SKILL.md.tmpl with {{PREAMBLE}}, +# {{MANIFEST_SCHEMA}}, and {{IDSTACK_RESOLVE}} placeholders. The preamble defines +# the AskUserQuestion / Skill / Agent concepts portably so skill bodies don't need +# per-target text substitution; {{IDSTACK_RESOLVE}} splices the canonical +# $_IDSTACK resolution (templates/snippets/idstack-resolve.sh) into each bash +# block that needs it — blocks run in separate shells and must be self-contained. set -e IDSTACK_DIR="$(cd "$(dirname "$0")/.." && pwd)" PREAMBLE="$IDSTACK_DIR/templates/preamble.md" MANIFEST_SCHEMA="$IDSTACK_DIR/templates/manifest-schema.md" +RESOLVE_SNIPPET="$IDSTACK_DIR/templates/snippets/idstack-resolve.sh" DRY_RUN=0 TARGET="all" GENERATED=0 @@ -58,14 +62,22 @@ if [ ! -f "$MANIFEST_SCHEMA" ]; then exit 1 fi +if [ ! -f "$RESOLVE_SNIPPET" ]; then + echo "ERROR: templates/snippets/idstack-resolve.sh not found at $RESOLVE_SNIPPET" >&2 + exit 1 +fi + # HTML report assets — skills read these at runtime to write branded reports -# into .idstack/exports//. Missing files would silently break -# every skill that produces a report, so fail loud here. +# into .idstack/exports//. report-format.md is the content +# contract the report-producing skills read alongside the visual template. +# Missing files would silently break every skill that produces a report, so +# fail loud here. REPORT_HTML_TMPL="$IDSTACK_DIR/templates/report.html.tmpl" INDEX_HTML_TMPL="$IDSTACK_DIR/templates/index.html.tmpl" REPORT_CSS="$IDSTACK_DIR/templates/assets/idstack.css" +REPORT_FORMAT="$IDSTACK_DIR/templates/report-format.md" -for f in "$REPORT_HTML_TMPL" "$INDEX_HTML_TMPL" "$REPORT_CSS"; do +for f in "$REPORT_HTML_TMPL" "$INDEX_HTML_TMPL" "$REPORT_CSS" "$REPORT_FORMAT"; do if [ ! -f "$f" ]; then echo "ERROR: report asset not found at ${f#$IDSTACK_DIR/}" >&2 exit 1 @@ -86,13 +98,17 @@ render_template() { skill_name="$(basename "$(dirname "$tmpl")")" if ! grep -q '{{PREAMBLE}}' "$tmpl"; then - echo " SKIP: $skill_name/SKILL.md.tmpl (no {{PREAMBLE}} placeholder)" + # A template without the placeholder would never be generated or checked — + # a silent SKIP here once let --dry-run pass green over an absent output. + echo " ERROR: $skill_name/SKILL.md.tmpl has no {{PREAMBLE}} placeholder" >&2 + ERRORS=$((ERRORS + 1)) return 1 fi local result result=$(awk -v header="$HEADER" -v header2="$HEADER2" \ -v preamble_file="$PREAMBLE" -v schema_file="$MANIFEST_SCHEMA" \ + -v resolve_file="$RESOLVE_SNIPPET" \ -v target="$target" ' BEGIN { after_frontmatter = 0 @@ -138,6 +154,14 @@ render_template() { close(schema_file) next } + # {{IDSTACK_RESOLVE}} appears many times per template (once per bash block + # that calls $_IDSTACK/bin/...); close() resets the read so each occurrence + # splices the full snippet. + /\{\{IDSTACK_RESOLVE\}\}/ { + while ((getline line < resolve_file) > 0) print line + close(resolve_file) + next + } { print } ' "$tmpl") @@ -183,9 +207,8 @@ render_target() { fi done - # Codex target: also emit AGENTS.md (memory file) and marketplace.json (plugin - # manifest). Renders in dry-run too, comparing against on-disk content so drift - # is visible to the smoke test. + # Codex target: also emit AGENTS.md (memory file). Renders in dry-run too, + # comparing against on-disk content so drift is visible to the smoke test. if [ "$target" = "codex" ]; then render_codex_assets fi @@ -225,16 +248,20 @@ case "$TARGET" in all) render_target claude; render_target codex ;; esac -if [ "$GENERATED" -eq 0 ]; then +if [ "$GENERATED" -eq 0 ] && [ "$ERRORS" -eq 0 ]; then echo "No .tmpl files found." exit 1 fi if [ "$DRY_RUN" -eq 1 ]; then echo "" - echo "$GENERATED templates checked. $ERRORS stale." + echo "$GENERATED templates checked. $ERRORS stale or failed." if [ "$ERRORS" -gt 0 ]; then exit 1; fi else echo "" echo "$GENERATED skill files generated (target: $TARGET)." + if [ "$ERRORS" -gt 0 ]; then + echo "ERROR: $ERRORS template(s) failed to render." >&2 + exit 1 + fi fi diff --git a/bin/idstack-migrate b/bin/idstack-migrate index da94c47..5df7fb0 100755 --- a/bin/idstack-migrate +++ b/bin/idstack-migrate @@ -5,12 +5,64 @@ # Prints "MIGRATED " to stderr when a migration is applied. # If the file does not exist, prints nothing and exits 0. # If python3 is unavailable, falls back to cat (graceful degradation). +# +# Usage: +# idstack-migrate [--init] [path] +# +# --init create a canonical manifest with every section at its default +# when none exists (still migrates in place when one does). Skills +# running standalone use this so a first run has something for +# bin/idstack-manifest-merge to merge into — that tool replaces one +# section and errors (exit 4) when the manifest is absent. +# +# The --init skeleton is produced by seeding a minimal v1.0 document and +# running the normal migration chain over it, so there is exactly one +# definition of "every section at its default" — the migrations themselves. set -e LATEST_VERSION="1.4" -MANIFEST="${1:-.idstack/project.json}" +INIT=0 +MANIFEST="" +for arg in "$@"; do + case "$arg" in + --init) INIT=1 ;; + -h|--help) + sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' + exit 0 ;; + -*) echo "idstack-migrate: unknown option: $arg" >&2; exit 2 ;; + *) [ -z "$MANIFEST" ] && MANIFEST="$arg" ;; + esac +done +MANIFEST="${MANIFEST:-.idstack/project.json}" + +# Seed a fresh manifest when asked. The migration chain below fills in every +# section, so this only has to carry the fields migrations do not default. +if [ ! -f "$MANIFEST" ] && [ "$INIT" = "1" ]; then + if ! command -v python3 &>/dev/null; then + echo "idstack-migrate: --init requires python3" >&2 + exit 3 + fi + mkdir -p "$(dirname "$MANIFEST")" + python3 -c " +import json, sys, datetime +ts = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') +seed = { + 'version': '1.0', + 'project_name': '', + 'created': ts, + 'updated': ts, + 'context': {}, + 'needs_analysis': {}, + 'learning_objectives': {}, +} +with open(sys.argv[1], 'w') as f: + json.dump(seed, f, indent=2) + f.write('\n') +" "$MANIFEST" + echo "INITIALIZED $MANIFEST" >&2 +fi # Nothing to do if no manifest exists [ -f "$MANIFEST" ] || exit 0 diff --git a/bin/idstack-status b/bin/idstack-status index 4535cbd..08b4226 100755 --- a/bin/idstack-status +++ b/bin/idstack-status @@ -45,7 +45,7 @@ if [ ! -f "$TIMELINE" ]; then echo "No timeline data. Run pipeline skills first." else echo "No course data yet." - echo "Run /needs-analysis to start a new course, or /course-import to bring in an existing one." + echo "Run /idstack:needs-analysis to start a new course, or /idstack:course-import to bring in an existing one." fi exit 0 fi @@ -60,8 +60,11 @@ if ! command -v python3 &>/dev/null; then fi if [ "$READINESS_ONLY" -eq 0 ]; then -python3 -c " -import json, sys +# PROJECT_NAME travels via the environment, never interpolated into Python +# source — a course name containing a quote character must not become a +# SyntaxError that blanks the dashboard. +PROJECT_NAME="$PROJECT_NAME" python3 -c " +import json, sys, os lines = open('$TIMELINE').readlines()[-200:] events = [] @@ -75,7 +78,7 @@ if not events: print('No valid timeline entries.') sys.exit(0) -print(f'Project: $PROJECT_NAME') +print('Project: ' + os.environ.get('PROJECT_NAME', 'unknown')) print() # Skills completed @@ -95,7 +98,7 @@ for s in pipeline: count = skills_run.get(s, 0) marker = 'x' if count > 0 else ' ' suffix = f' ({count}x)' if count > 1 else '' - print(f' [{marker}] /{s}{suffix}') + print(f' [{marker}] /idstack:{s}{suffix}') print() # Quality score trend @@ -115,10 +118,12 @@ if quality_reviews: print(f'Cross-domain: CogLoad={cd.get(\"cognitive_load\",\"?\")}/10 Multimedia={cd.get(\"multimedia\",\"?\")}/10 Feedback={cd.get(\"feedback_quality\",\"?\")}/10') print() -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. completed = set(skills_run.keys()) pipeline_order = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -135,9 +140,9 @@ if not next_skill and 'needs-analysis' not in completed and 'course-import' not next_skill = 'needs-analysis' if next_skill: - print(f'Suggested next: /{next_skill}') + print(f'Suggested next: /idstack:{next_skill}') print() -" 2>/dev/null || echo "Error reading timeline." +" || echo "Error reading timeline." fi # Learnings count @@ -264,9 +269,9 @@ print() # with itemized reasons. Missing reviews produce INCOMPLETE. def verdict(): missing = [] - if not quality_reviewed: missing.append('/course-quality-review') - if not red_reviewed: missing.append('/red-team') - if not access_reviewed: missing.append('/accessibility-review') + if not quality_reviewed: missing.append('/idstack:course-quality-review') + if not red_reviewed: missing.append('/idstack:red-team') + if not access_reviewed: missing.append('/idstack:accessibility-review') if missing: return f\"INCOMPLETE (run {', '.join(missing)})\" @@ -285,5 +290,5 @@ def verdict(): print(f' Verdict: {verdict()}') print() -" 2>/dev/null || true +" || echo " (readiness computation failed)" fi diff --git a/bin/lib/plugin-status.sh b/bin/lib/plugin-status.sh new file mode 100644 index 0000000..a519fc9 --- /dev/null +++ b/bin/lib/plugin-status.sh @@ -0,0 +1,25 @@ +# Plugin-status parsing for `claude plugin list` output — the single definition +# used by bin/idstack-doctor. Source this file; do not inline the awk program +# (an inlined copy cannot be unit-tested, and the version-classifier drifted +# three times for exactly that reason). +# +# plugin_entry_window reads `claude plugin list` output on stdin and +# prints only the entry for : the matching line plus its indented +# continuation lines, stopping at a blank line or the next `name@marketplace` +# id. Scoping matters — a fixed-size window (the old `grep -A4`) reads a +# neighboring plugin's status line and reports a disabled plugin as enabled +# whenever the next entry starts within the window. +plugin_entry_window() { + awk -v id="$1" ' + index($0, id) && !f { f = 1; print; next } + f { + if ($0 ~ /^[[:space:]]*$/ || $0 ~ /[A-Za-z0-9._-]+@[A-Za-z0-9._-]+/) { f = 0 } + else { print } + }' +} + +# plugin_is_enabled — stdin is `claude plugin list` output. +# Returns 0 when that plugin's own entry reports enabled, 1 otherwise. +plugin_is_enabled() { + plugin_entry_window "$1" | grep -qi "enabled" +} diff --git a/bin/lib/version-classify.sh b/bin/lib/version-classify.sh new file mode 100644 index 0000000..213f879 --- /dev/null +++ b/bin/lib/version-classify.sh @@ -0,0 +1,21 @@ +# Legacy-VERSION classifier — the single definition shared by setup, +# bin/idstack-doctor, and test/test-version-classifier.sh. Source this file; +# do not copy the case statement (copies drifted three times across +# PR #15 / #19 / #20 / #21 before it was extracted here). +# +# classify_version prints one of: +# skip — modern install (v2.0.1.0+), leave alone +# legacy — pre-v2.0.1.0 install, flag for cleanup +# unknown — unparseable, leave alone and warn +# +# Patterns are bash globs, not regex. Two arms: explicitly skip modern/future +# versions first, then flag the legacy ones. 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) classify correctly. Pinned by test/test-version-classifier.sh. +classify_version() { + case "$1" in + 2.0.[1-9]*|2.[1-9]*|[3-9]*|[1-9][0-9]*) echo "skip" ;; + 0.*|1.*|2.0.0.*|2.0.0) echo "legacy" ;; + *) echo "unknown" ;; + esac +} diff --git a/dist/codex/skills/idstack-accessibility-review/SKILL.md b/dist/codex/skills/idstack-accessibility-review/SKILL.md index dc8d8ff..d8159f4 100644 --- a/dist/codex/skills/idstack-accessibility-review/SKILL.md +++ b/dist/codex/skills/idstack-accessibility-review/SKILL.md @@ -41,23 +41,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -69,6 +72,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -138,6 +155,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -171,7 +202,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -179,9 +212,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -259,33 +294,17 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest +## Manifest Inputs (skill-specific) -Before starting the review, check for an existing project manifest. - -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to - the user, offer to fix it, and STOP until it is valid. Never silently overwrite - corrupt JSON. - Check which sections are populated. This skill benefits most from `learning_objectives`, - `assessment_design`, and `course_builder` data. -- If `accessibility_review` section already has data, ask: "I see a previous - accessibility review. Want to update it or start fresh?" -- Preserve all existing sections when writing back. - -**If NO_MANIFEST:** -- That is fine. This skill works standalone. Gather course information through - AskUserQuestion. You will create the manifest at the end if the user wants to - save results. + `assessments`, and `course_content` data. +- If no manifest: this skill works standalone. Gather course information through + AskUserQuestion. At the end, if the user wants the results saved, create the + manifest first (`bin/idstack-migrate --init`, see Write Manifest) — the merge + tool needs one to merge into. --- @@ -515,6 +534,24 @@ Calculate the accessibility score (0-100): Generate an HTML report so the designer has a single document covering both compliance (WCAG, the Must Fix layer) and inclusion (UDL, the Should Improve layer). The report follows the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -547,6 +584,24 @@ and atomically updates the top-level `updated` timestamp. The payload must inclu `report_path` set to the value of `$_REPORT_PATH` from the bash block above (e.g., `.idstack/exports//accessibility-review.html`). ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-manifest-merge" --section accessibility_review --payload - <<'PAYLOAD' { "updated": "", @@ -575,10 +630,35 @@ and atomically updates the top-level `updated` timestamp. The payload must inclu PAYLOAD ``` -If `.idstack/project.json` doesn't exist yet, run `bin/idstack-migrate .idstack/project.json` -first — that creates a fresh canonical manifest. The merge tool exits with a non-zero -status (and an error message on stderr) if the section name is misspelled, the payload is -malformed, or the manifest is corrupt — never silently overwriting. +If `.idstack/project.json` doesn't exist yet — the standalone case this skill explicitly +supports — create it first with `--init`, which writes a canonical manifest with every +section at its default. Without one the merge tool exits 4: + +```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done +"$_IDSTACK/bin/idstack-migrate" --init .idstack/project.json >/dev/null +``` + +The merge tool exits with a non-zero status (and an error message on stderr) if the +section name is misspelled, the payload is malformed, or the manifest is corrupt — +never silently overwriting. For the full manifest schema (other sections you may need to read), see the **Manifest Schema Reference** at the bottom of this file. @@ -619,7 +699,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -630,10 +710,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1008,6 +1110,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"accessibility-review","event":"completed"}' ``` @@ -1018,5 +1138,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"accessibility-review","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/dist/codex/skills/idstack-assessment-design/SKILL.md b/dist/codex/skills/idstack-assessment-design/SKILL.md index 83caf0b..7d3a224 100644 --- a/dist/codex/skills/idstack-assessment-design/SKILL.md +++ b/dist/codex/skills/idstack-assessment-design/SKILL.md @@ -41,23 +41,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -69,6 +72,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -138,6 +155,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -171,7 +202,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -179,9 +212,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -225,7 +260,7 @@ Example: "Reminder: this Canvas instance uses custom rubric formatting (discover --- -**Skill-specific manifest check:** If the manifest `assessment_design` section already has data, +**Skill-specific manifest check:** If the manifest `assessments` section already has data, ask the user: "I see you've already run this skill. Want to update the results or start fresh?" # Assessment Design — Rubrics, Feedback Strategies & Formative Checkpoints @@ -314,34 +349,20 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest - -Before starting assessment design, check for an existing project manifest. - -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` +## Manifest Inputs (skill-specific) -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to the - user, offer to fix it, and STOP until it is valid. Never silently overwrite corrupt JSON. -- If `assessments` section already has data (non-empty `items` array), ask: - "I see you've already designed assessments. Want to update them or start fresh?" -- Preserve all existing sections when writing back. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. **If NO_MANIFEST:** -- Say: "I see you haven't run `/learning-objectives` yet. Running it first gives me +- Say: "I see you haven't run `/idstack:learning-objectives` yet. Running it first gives me your ILOs with Bloom's classifications, which helps me recommend assessment types that actually measure your stated outcomes. Want to continue anyway, or run - `/learning-objectives` first?" + `/idstack:learning-objectives` first?" - If the user wants to continue, proceed without manifest context. You can still - design assessments; you just won't have the upstream alignment data. -- You will create the manifest at the end of this skill's workflow. + design assessments; you just won't have the upstream alignment data. The manifest + gets created at the end so results persist (`bin/idstack-migrate --init`, see + Write Manifest) — the merge tool needs one to merge into. --- @@ -382,7 +403,7 @@ assessments to match."** For each objective provided, classify on both Bloom's dimensions (knowledge and cognitive process) before proceeding to assessment design. Use the same classification -approach as the `/learning-objectives` skill: ask for clarification when verbs are +approach as the `/idstack:learning-objectives` skill: ask for clarification when verbs are ambiguous [Alignment-12] [T2]. ### Mode 3: Audit Existing Assessments @@ -576,9 +597,9 @@ and close performance gaps before they matter [Assessment-9] [T5]. - **Feedback type:** Immediate and elaborated whenever possible. Automated feedback is acceptable for remember/understand levels. Higher levels need human or structured peer feedback [Assessment-10] [T1]. -- **Purpose:** Close the gap between current and desired performance [Assessment-9] - [T5]. Each checkpoint should give students evidence of where they stand relative - to the rubric criteria. +- **Purpose:** Close the gap between current and desired performance + [Assessment-9] [T5]. Each checkpoint should give students evidence of where they + stand relative to the rubric criteria. **Student-initiated formative assessment:** Where possible, design checkpoints that students can initiate on their own (practice @@ -658,6 +679,24 @@ Consider adding a formative peer review checkpoint to partially address the gap. Before writing the manifest, generate an HTML report so the designer has a single document to read. The report follows the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -698,6 +737,24 @@ first, merge in your changes, then pass the full updated section as the payload. The `assessments` payload must include `report_path` set to the value of `$_REPORT_PATH` from the bash block above (e.g., `.idstack/exports//assessment-design.html`). ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Section 1: assessments "$_IDSTACK/bin/idstack-manifest-merge" --section assessments --payload - <<'PAYLOAD' @@ -713,7 +770,29 @@ PAYLOAD If `bin/idstack-manifest-merge` is unavailable: fall back to manual write (Read manifest, modify only the two sections, Write back, preserve all others). -If `.idstack/project.json` does not exist yet, run `bin/idstack-migrate .idstack/project.json` first — that creates a fresh canonical manifest. The merge tool then merges into it. +If `.idstack/project.json` does not exist yet — the standalone case, where the user chose to continue without upstream data — create it first with `--init`, which writes a canonical manifest with every section at its default. The merge tool then merges into it (without a manifest it exits 4): + +```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done +"$_IDSTACK/bin/idstack-migrate" --init .idstack/project.json >/dev/null +``` **Populate the `assessments` section:** @@ -798,7 +877,7 @@ Write the manifest, then confirm to the user: alignment notes. Open it in any browser; the folder is self-contained. - System state: `.idstack/project.json` (the manifest — for downstream skills). -**Next step:** Run `/course-builder` to generate the full course content including +**Next step:** Run `/idstack:course-builder` to generate the full course content including assessment documents, rubric handouts, and assignment instructions." --- @@ -816,7 +895,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -827,10 +906,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1205,6 +1306,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"assessment-design","event":"completed"}' ``` @@ -1215,5 +1334,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"assessment-design","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/dist/codex/skills/idstack-course-builder/SKILL.md b/dist/codex/skills/idstack-course-builder/SKILL.md index ef17203..6720128 100644 --- a/dist/codex/skills/idstack-course-builder/SKILL.md +++ b/dist/codex/skills/idstack-course-builder/SKILL.md @@ -41,23 +41,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -69,6 +72,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -138,6 +155,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -171,7 +202,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -179,9 +212,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -225,7 +260,7 @@ Example: "Reminder: this Canvas instance uses custom rubric formatting (discover --- -**Skill-specific manifest check:** If the manifest `course_builder` section already has data, +**Skill-specific manifest check:** If the manifest `course_content` section already has data, ask the user: "I see you've already run this skill. Want to update the results or start fresh?" # Course Builder — Evidence-Based Content Generation @@ -316,32 +351,19 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest - -Before starting content generation, check for an existing project manifest. +## Manifest Inputs (skill-specific) -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. **If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to the - user, offer to fix it, and STOP until it is valid. Never silently overwrite corrupt JSON. - Check which sections are populated. At minimum, you need: - `learning_objectives.ilos` — a non-empty array of classified objectives - `context` — at least `modality` and `timeline` -- If `course_content` section already has data, ask: "I see you've already generated - course content. Want to regenerate from scratch or update specific files?" -- Preserve all existing sections when writing back. **If NO_MANIFEST:** - Say: "I need a project manifest with learning objectives to generate course content. - Run `/needs-analysis` followed by `/learning-objectives` to build the foundation. + Run `/idstack:needs-analysis` followed by `/idstack:learning-objectives` to build the foundation. If you have objectives ready, I can create a minimal manifest to work from — just tell me your learning objectives, course modality, and timeline." - If the user provides objectives directly, create a minimal manifest and proceed. @@ -350,11 +372,11 @@ fi **Nudge for assessment design:** If the manifest exists but has no `assessments` section (or it is empty), say: -"I notice you haven't run `/assessment-design` yet. I can generate basic assessment -documents from the alignment matrix in your objectives, but running `/assessment-design` +"I notice you haven't run `/idstack:assessment-design` yet. I can generate basic assessment +documents from the alignment matrix in your objectives, but running `/idstack:assessment-design` first would give me richer assessment data — rubric criteria, feedback strategies, and assessment type recommendations. Want to continue with what I have, or run -`/assessment-design` first?" +`/idstack:assessment-design` first?" --- @@ -369,7 +391,7 @@ If the manifest exists with upstream data, use it to inform content generation. - **Assessments:** [count] assessments [or 'none — I will derive from alignment matrix'] - **Modality:** [online/hybrid/face-to-face] - **Timeline:** [duration] -- **Expertise reversal flags:** [any flags from /learning-objectives] +- **Expertise reversal flags:** [any flags from /idstack:learning-objectives] Here is what I will generate:" @@ -723,7 +745,7 @@ describe it here.] - Each criterion should map to a specific ILO or component of an ILO - Performance levels should describe observable differences, not just degree words ("excellent analysis" vs. "good analysis" is not useful) -- If the manifest has rubric data from `/assessment-design`, use it directly +- If the manifest has rubric data from `/idstack:assessment-design`, use it directly - If generating rubrics from scratch, ensure the cognitive level of each criterion matches the ILO it measures [Alignment-1] [T5] @@ -768,6 +790,24 @@ continue with remaining files. Before updating the manifest, generate an HTML build report so the designer can see what was generated and why each design choice was made (or, in gap-fill mode, what was skipped and why). The report follows the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -795,6 +835,24 @@ Write the HTML report at the path printed above (`.idstack/exports/ Save the `course_content` section to `.idstack/project.json` via `bin/idstack-manifest-merge`. The merge tool replaces only the named section, preserves every other section verbatim, validates JSON, and atomically updates the top-level `updated` timestamp. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-manifest-merge" --section course_content --payload - <<'PAYLOAD' { "report_path": "/course-builder.html>", @@ -833,7 +891,7 @@ The payload is the **flat section contents** (no enclosing `course_content` key) **Mode field.** Set `mode` to `"build-new"` for a from-scratch generation or `"gap-fill"` when this run was triggered by upstream skills flagging missing artifacts. In `gap-fill` mode, populate `recommended_generation_targets[]` with the per-target outcomes (`status`: `generated|deferred|declined`); in `build-new` mode, leave the array empty. -The `placeholders_used` array lists any fields where placeholder text was used because the user chose to skip or defer those details. This helps downstream skills (like `/course-export`) know what still needs to be filled in. +The `placeholders_used` array lists any fields where placeholder text was used because the user chose to skip or defer those details. This helps downstream skills (like `/idstack:course-export`) know what still needs to be filled in. **Fallback (if `bin/idstack-manifest-merge` is unavailable):** Read the full manifest, modify only the `course_content` section, Write back. Preserve all other sections verbatim. @@ -850,8 +908,8 @@ After writing the manifest, confirm: **Next steps:** - Review and edit the generated files to add your expertise and institutional voice. -- Run `/course-quality-review` to audit the complete course against QM standards and CoI presence. -- Run `/course-export` to package the content as an IMS Common Cartridge or push to Canvas." +- Run `/idstack:course-quality-review` to audit the complete course against QM standards and CoI presence. +- Run `/idstack:course-export` to package the content as an IMS Common Cartridge or push to Canvas." --- @@ -895,7 +953,7 @@ user, run an adversarial self-review if the **Agent tool** is available. - If critical issues remain that require user input, list them in the summary. **If Agent tool is NOT available:** Skip this step. Add a note to the output: -"Tip: Run `/idstack course-quality-review` next for a full alignment audit." +"Tip: Run `/idstack:course-quality-review` next for a full alignment audit." --- @@ -912,7 +970,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -923,10 +981,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1301,6 +1381,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"course-builder","event":"completed"}' ``` @@ -1311,5 +1409,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"course-builder","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/dist/codex/skills/idstack-course-export/SKILL.md b/dist/codex/skills/idstack-course-export/SKILL.md index c0adc4c..6944f24 100644 --- a/dist/codex/skills/idstack-course-export/SKILL.md +++ b/dist/codex/skills/idstack-course-export/SKILL.md @@ -41,23 +41,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -69,6 +72,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -138,6 +155,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -171,7 +202,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -179,9 +212,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -225,7 +260,7 @@ Example: "Reminder: this Canvas instance uses custom rubric formatting (discover --- -**Skill-specific manifest check:** If the manifest `course_export` section already has data, +**Skill-specific manifest check:** If the manifest `export_metadata` section already has data, ask the user: "I see you've already run this skill. Want to update the results or start fresh?" ## Pre-Export Readiness Check @@ -233,7 +268,24 @@ ask the user: "I see you've already run this skill. Want to update the results o Before starting the export workflow, run the readiness dashboard: ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-status" --readiness ``` @@ -247,7 +299,7 @@ This is advisory — the user can always choose to export regardless. # Course Export — IMS Common Cartridge & Canvas API You are a course export partner. Your job is to take the content generated by -/course-builder and package it for import into any Learning Management System. +/idstack:course-builder and package it for import into any Learning Management System. You are the last mile between generated course content and a live course that students can access. @@ -267,7 +319,7 @@ manifest that ties them together. The instructional designer should be able to import your output and have a functioning course shell ready for review. You read from two sources: -- The `.idstack/course-content/` directory, where /course-builder writes its +- The `.idstack/course-content/` directory, where /idstack:course-builder writes its generated files (syllabus, module content, assessments, rubrics) - The `.idstack/project.json` manifest, which contains the course structure, learning objectives, and alignment data from upstream skills @@ -285,18 +337,12 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest and Course Content +## Course Content Inputs -Before starting the export, verify that generated course content exists. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` +Before starting the export, verify that generated course content exists: ```bash if [ -d ".idstack/course-content" ]; then @@ -308,20 +354,13 @@ fi ``` **If NO_MANIFEST or NO_CONTENT:** -Tell the user: "I need generated course content to export. Run `/course-builder` +Tell the user: "I need generated course content to export. Run `/idstack:course-builder` first to generate your syllabus, modules, and assessments. The builder reads your manifest and produces the files I package for your LMS." **If MANIFEST_EXISTS but no `course_content` section:** Check whether `.idstack/course-content/` has files. If it does, proceed using -the files directly. If not, nudge for /course-builder. - -**If both exist:** -Read the manifest. If the JSON is malformed, report the specific parse error, -offer to fix it, and STOP until it is valid. Never silently proceed with -corrupt data. - -Preserve all existing manifest sections when writing back. +the files directly. If not, nudge for /idstack:course-builder. --- @@ -333,6 +372,24 @@ produced by the rest of the pipeline. Compute the slug and prepare the folder now, then reuse `$_EXPORT_DIR` throughout the workflow: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Course-slug-based export folder. Required before any artifact write. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -903,7 +960,7 @@ If any items failed: |------|------|-------| | {name} | {type} | {error message} | -You can create these items manually in Canvas, or run `/course-export` again +You can create these items manually in Canvas, or run `/idstack:course-export` again to retry the failed items. ``` @@ -920,7 +977,7 @@ find .idstack/course-content/ -type f | sort ``` If no course content files exist, tell the user: "No course content found in -`.idstack/course-content/`. Run `/course-builder` first to generate content." +`.idstack/course-content/`. Run `/idstack:course-builder` first to generate content." ### C2. Create SCORM package structure @@ -1060,7 +1117,7 @@ Total files: [count] - SCORM API tracking (completion, score reporting) is not included. The LMS will mark the SCO as complete when the learner opens it. - For richer interactivity, author in Articulate Rise or Storyline and - use idstack's /course-quality-review and /red-team on the exported package. + use idstack's /idstack:course-quality-review and /idstack:red-team on the exported package. ``` ### C8. Cleanup @@ -1092,18 +1149,45 @@ After the export completes (any path), write the HTML report at `$_REPORT_PATH` After export completes (any path) and the report is written, update the project manifest with export metadata. +Save results via the section-scoped merge tool — it validates the section name +against the canonical schema, preserves every other section verbatim, bumps the +top-level `updated` timestamp, and writes atomically: + +```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done +# payload file holds ONLY the export_metadata section object (shape below) +"$_IDSTACK/bin/idstack-manifest-merge" --section export_metadata --payload .idstack/.export_metadata-payload.json && rm -f .idstack/.export_metadata-payload.json +``` + +Write the payload object to `.idstack/.export_metadata-payload.json` with the +Write tool first, then run the merge. **If no manifest exists yet** (unlikely +for export, but possible), the merge tool exits with code 4 (`manifest not +found`) — in that case fall back to creating `.idstack/project.json` with the +Write tool, initializing ALL sections from the schema with empty/default values +and placing your `export_metadata` data inside it. + **CRITICAL -- Manifest Integrity Rules:** -1. If a manifest already exists, READ it first with the Read tool. -2. Modify ONLY the `export_metadata` section and the `updated` timestamp. - Preserve all other sections unchanged — `context`, `needs_analysis`, - `learning_objectives`, `quality_review`, `import_metadata`, and any other - sections must remain exactly as they were. -3. Before writing, verify the JSON is valid: matching braces, proper commas, - quoted strings, no trailing commas. -4. Update the top-level `updated` timestamp to reflect the current time. -5. If this is a new manifest (unlikely for export, but possible), initialize - ALL sections with empty/default values so downstream skills find the - expected structure. +1. The payload must be valid JSON: matching braces, proper commas, quoted + strings, no trailing commas. +2. NEVER write secrets (Canvas tokens, credentials) to the manifest or the + payload file. ### Readiness Info @@ -1117,14 +1201,15 @@ Export readiness: Accessibility review: WCAG score 70/100, 1 AA violation ``` -If a section doesn't exist, show: "Not reviewed — run /[skill-name] for analysis." +If a section doesn't exist, show: "Not reviewed — run /idstack: for analysis." This is informational. Export proceeds regardless. The user can choose to address findings first or export now, no AskUserQuestion needed, just show the info and continue. ### Write Export Metadata -Add or update the `export_metadata` field at the root level: +The payload file holds the `export_metadata` section object (shown here wrapped +in its manifest key for context — the payload file contains only the inner object): ```json { @@ -1163,7 +1248,7 @@ sections from the manifest (if they exist). The `verdict` is: - `export_blocked`: not used (export never blocks, advisory only) - Empty string if no reviews exist -Write the manifest, then confirm: +Run the merge, then confirm: "Your export metadata has been saved to `.idstack/project.json`. @@ -1189,7 +1274,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -1200,10 +1285,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1578,6 +1685,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"course-export","event":"completed"}' ``` @@ -1588,5 +1713,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"course-export","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/dist/codex/skills/idstack-course-import/SKILL.md b/dist/codex/skills/idstack-course-import/SKILL.md index 4ca6cdb..e2d21f9 100644 --- a/dist/codex/skills/idstack-course-import/SKILL.md +++ b/dist/codex/skills/idstack-course-import/SKILL.md @@ -41,23 +41,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -69,6 +72,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -138,6 +155,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -171,7 +202,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -179,9 +212,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -225,7 +260,7 @@ Example: "Reminder: this Canvas instance uses custom rubric formatting (discover --- -**Skill-specific manifest check:** If the manifest `course_import` section already has data, +**Skill-specific manifest check:** If the manifest `import_metadata` section already has data, ask the user: "I see you've already run this skill. Want to update the results or start fresh?" # Course Import — Universal LMS Course Import @@ -252,7 +287,7 @@ This skill draws primarily from Domain 10 (Online Course Quality) and Domain 2 should detect alignment (or its absence) from the source data. - The revised Bloom's taxonomy (Anderson & Krathwohl) classifies objectives on two dimensions: knowledge type and cognitive process [Alignment-7] [T3]. - Pre-classification during import saves time for /learning-objectives. + Pre-classification during import saves time for /idstack:learning-objectives. ## Evidence Tier Key @@ -265,32 +300,16 @@ Every recommendation and flag includes its evidence tier: --- -## Preamble: Project Manifest - -Before starting the import, check for an existing project manifest. - -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` +## Manifest Inputs (skill-specific) -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error, - offer to fix it, and STOP until it is valid. -- Check which sections have data: - - If `needs_analysis` has data from /needs-analysis: note it. You will PRESERVE - this data. Import adds to task_analysis and learner_profile, does not replace. - - If `learning_objectives` has data from /learning-objectives: ask the user - "You already have learning objectives in your manifest. Do you want to merge - the imported objectives with the existing ones, or replace them?" -- Preserve all sections you don't write to. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. -**If NO_MANIFEST:** -- You will create the manifest at the end of this skill's workflow. +- If `needs_analysis` has data from /idstack:needs-analysis: note it. You will PRESERVE + this data. Import adds to task_analysis and learner_profile, does not replace. +- If `learning_objectives` has data from /idstack:learning-objectives: ask the user + "You already have learning objectives in your manifest. Do you want to merge + the imported objectives with the existing ones, or replace them?" --- @@ -623,7 +642,7 @@ it for up to 10 pages (500 items max per endpoint). After 500 items, stop and no **Error handling for each call:** - 404: Skip this endpoint, note what's missing - 429: Wait 10 seconds, retry once. If still 429: "Canvas is rate-limiting. - Wait a minute and try `/course-import` again." + Wait a minute and try `/idstack:course-import` again." - Timeout: "Canvas didn't respond for [endpoint]. Continuing with what we have." ### C4. Map API response to course structure @@ -786,7 +805,7 @@ Continue to Step 2 (Quality Flags). ## Step 2: Quick-Scan Quality Flags After extracting course structure from ANY input method, scan for obvious quality -issues. This is NOT a full /course-quality-review. This is a quick triage that +issues. This is NOT a full /idstack:course-quality-review. This is a quick triage that flags problems visible in the structural data alone. **Structural flags:** @@ -823,11 +842,11 @@ Found {N} flags during import: {list each flag with ⚠ prefix} These are quick observations from the course structure, not a full review. -Run /course-quality-review for an evidence-based audit with specific recommendations. +Run /idstack:course-quality-review for an evidence-based audit with specific recommendations. ``` If zero flags: "No obvious structural issues detected during import. Run -/course-quality-review for a deeper analysis." +/idstack:course-quality-review for a deeper analysis." --- @@ -905,10 +924,10 @@ For each objective: 4. **Confidence level:** - High: verb maps clearly to one Bloom's level - Ambiguous: verb could map to multiple levels (e.g., "analyze", "demonstrate") - Mark as "verify with /learning-objectives" [Alignment-12] [T2] + Mark as "verify with /idstack:learning-objectives" [Alignment-12] [T2] 5. **Set alignment_status to "imported-unverified"** — the user should run - /learning-objectives to verify and check bidirectional alignment + /idstack:learning-objectives to verify and check bidirectional alignment Assign ILO IDs: ILO-1, ILO-2, etc. @@ -923,7 +942,7 @@ Present for review: | ILO-2 | [text] | [dim] | [proc] | ambiguous — verify | ... -All classifications are marked "imported-unverified." Run /learning-objectives +All classifications are marked "imported-unverified." Run /idstack:learning-objectives to verify Bloom's levels and check alignment with activities and assessments. ``` @@ -934,6 +953,24 @@ to verify Bloom's levels and check alignment with activities and assessments. Before writing the manifest, generate an HTML report so the designer has a single document about what came in and where the quality flags are. The report follows the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -971,7 +1008,7 @@ Create or update the project manifest. **CRITICAL — Manifest Integrity Rules:** 1. If a manifest already exists, READ it first. 2. PRESERVE sections populated by other skills (especially needs_analysis from - /needs-analysis). Import ADDS to these sections, does not replace. + /idstack:needs-analysis). Import ADDS to these sections, does not replace. 3. If learning_objectives already has data and the user chose "merge," combine imported ILOs with existing ones (use new ILO IDs that don't conflict). 4. Include the COMPLETE schema structure. Do not omit fields. @@ -979,7 +1016,7 @@ Create or update the project manifest. 6. Update the `updated` timestamp. 7. **NEVER write the Canvas API token to the manifest or any file.** -**Fields populated by /course-import:** +**Fields populated by /idstack:course-import:** - `project_name` — from course title - `context.modality` — inferred from course structure (async=online, sync sessions=hybrid) @@ -1034,11 +1071,11 @@ Write the manifest, then confirm: - System state: `.idstack/project.json` (the manifest — for downstream skills). **Recommended next steps:** -1. `/course-quality-review` — Full evidence-based audit with QM standards and +1. `/idstack:course-quality-review` — Full evidence-based audit with QM standards and CoI presence analysis -2. `/learning-objectives` — Verify Bloom's classifications and check +2. `/idstack:learning-objectives` — Verify Bloom's classifications and check bidirectional alignment (objectives ↔ activities ↔ assessments) -3. `/needs-analysis` — Add organizational context and learner profile data +3. `/idstack:needs-analysis` — Add organizational context and learner profile data that can't be extracted from the course structure alone ``` @@ -1057,7 +1094,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -1068,10 +1105,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1446,6 +1505,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"course-import","event":"completed"}' ``` @@ -1456,5 +1533,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"course-import","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/dist/codex/skills/idstack-course-quality-review/SKILL.md b/dist/codex/skills/idstack-course-quality-review/SKILL.md index 6ad680c..2e1f37f 100644 --- a/dist/codex/skills/idstack-course-quality-review/SKILL.md +++ b/dist/codex/skills/idstack-course-quality-review/SKILL.md @@ -41,23 +41,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -69,6 +72,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -138,6 +155,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -171,7 +202,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -179,9 +212,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -225,7 +260,7 @@ Example: "Reminder: this Canvas instance uses custom rubric formatting (discover --- -**Skill-specific manifest check:** If the manifest `course_quality_review` section already has data, +**Skill-specific manifest check:** If the manifest `quality_review` section already has data, ask the user: "I see you've already run this skill. Want to update the results or start fresh?" # Course Quality Review — QM-Aligned Audit with CoI Presence Layer @@ -291,32 +326,14 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest +## Manifest Inputs (skill-specific) -Before starting the review, check for an existing project manifest. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` - -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to - the user, offer to fix it, and STOP until it is valid. Never silently overwrite - corrupt JSON. - Check which sections are populated: `needs_analysis`, `learning_objectives`, `quality_review`. This determines your review mode. -- If `quality_review` section already has data, ask: "I see a previous quality - review. Want to update it or start fresh?" -- Preserve all existing sections when writing back. - -**If NO_MANIFEST:** -- That is fine. This skill works standalone. You will create the manifest at the - end if the user wants to save results. +- Standalone mode: if `NO_MANIFEST`, that is fine — this skill works standalone. --- @@ -653,24 +670,24 @@ Scan the course design for these cognitive load violations: - **Split attention:** Content explanation is separated from the diagram, example, or visual it references. Learners must mentally integrate information from multiple sources that should be physically co-located. Flag as warning. - Fix: run /course-builder to regenerate module with integrated content. + Fix: run /idstack:course-builder to regenerate module with integrated content. - **Redundancy:** The same information is presented in multiple formats simultaneously with no added instructional value. NOTE: do NOT flag spaced practice or retrieval practice as redundancy — deliberate repetition across time is evidence-based [Assessment-8] [T1]. Only flag identical information presented simultaneously (e.g., reading aloud on-screen text verbatim). Flag as info. - Fix: run /course-builder to consolidate redundant presentations. + Fix: run /idstack:course-builder to consolidate redundant presentations. - **Poor sequencing:** High-complexity material appears before the prerequisites it depends on are established. Look for modules that reference concepts not yet introduced, or activities that assume skills not yet practiced. Flag as critical. - Fix: run /course-builder to resequence modules based on prerequisite chain. + Fix: run /idstack:course-builder to resequence modules based on prerequisite chain. - **Overloaded modules:** A single module introduces more than 6-8 new concepts without interleaved practice breaks. Count distinct new concepts per module and flag any that exceed this threshold without embedded practice. Flag as warning. - Fix: run /course-builder to split module or add practice checkpoints. + Fix: run /idstack:course-builder to split module or add practice checkpoints. ### Check 2: Multimedia Principle Violations @@ -681,22 +698,22 @@ Scan for violations of Mayer's multimedia learning principles: - **Spatial contiguity:** Text and related visuals are physically separated (e.g., figure on one page, explanation on another; caption far from image). Flag as warning. - Fix: run /course-builder to co-locate text and visuals. + Fix: run /idstack:course-builder to co-locate text and visuals. - **Temporal contiguity:** Narration and visuals are not synchronized (e.g., a video describes a diagram that appears 30 seconds later). Flag as warning. - Fix: run /course-builder to synchronize narration with visual presentation. + Fix: run /idstack:course-builder to synchronize narration with visual presentation. - **Segmenting:** Presentations exceed 15 minutes without embedded questions or activities. Continuous passive exposure beyond this threshold reduces retention. Flag as warning. - Fix: run /course-builder to segment long presentations with embedded activities. + Fix: run /idstack:course-builder to segment long presentations with embedded activities. - **Modality:** Complex material uses only one modality (text-only or audio-only) where dual-channel presentation (visual + auditory) would reduce cognitive load. Flag as info. - Fix: run /course-builder to add complementary modality. + Fix: run /idstack:course-builder to add complementary modality. - **Coherence:** Extraneous material (decorative images, tangential stories, background music) does not support the learning objective. Seductive details hurt learning. Flag as info. - Fix: run /course-builder to remove extraneous elements. + Fix: run /idstack:course-builder to remove extraneous elements. ### Check 3: Feedback Quality @@ -709,19 +726,19 @@ Scan the assessment design for feedback quality issues: creation that only report correct/incorrect provide no learning mechanism. Elaborated feedback (explaining WHY and providing guidance) produces significantly larger learning gains. Flag as critical. - Fix: run /assessment-design to add elaborated feedback for higher-order assessments. + Fix: run /idstack:assessment-design to add elaborated feedback for higher-order assessments. - **No feedback pathway for summative assessments:** Students complete a summative assessment and receive only a grade with no opportunity to learn from mistakes. Flag as warning. - Fix: run /assessment-design to add post-submission feedback or reflection activity. + Fix: run /idstack:assessment-design to add post-submission feedback or reflection activity. - **Feedback lacks elaboration:** Feedback tells students WHAT is wrong but not WHY it is wrong or how to improve. Flag as warning. - Fix: run /assessment-design to add elaborated feedback with explanations. + Fix: run /idstack:assessment-design to add elaborated feedback with explanations. - **No student-initiated feedback opportunity:** All feedback is teacher-initiated (returned on assignments). There is no mechanism for students to seek feedback when they need it (e.g., self-check quizzes, rubric previews, peer review). Flag as info. - Fix: run /assessment-design to add formative self-check opportunities. + Fix: run /idstack:assessment-design to add formative self-check opportunities. ### Check 4: Expertise Reversal @@ -730,29 +747,29 @@ Scan the assessment design for feedback quality issues: If a learner profile is available (from manifest `needs_analysis.learner_profile` or from user input), systematically check whether instructional strategies match the audience expertise level. If no learner profile exists, flag the absence as -a warning and recommend running /needs-analysis. +a warning and recommend running /idstack:needs-analysis. - **Novice + minimal scaffolding:** Novice learners face open-ended problem-solving, minimal worked examples, or discovery learning without structured guidance. This causes cognitive overload and poor learning outcomes. Flag as critical. - Fix: run /course-builder to regenerate module with scaffolding and worked examples. + Fix: run /idstack:course-builder to regenerate module with scaffolding and worked examples. - **Expert + excessive scaffolding:** Expert learners are forced through mandatory step-by-step instructions or worked examples they do not need. Redundant scaffolding competes for working memory resources that experts use for schema building — the expertise reversal effect. Flag as warning. - Fix: run /course-builder to offer advanced-track options that skip scaffolding. + Fix: run /idstack:course-builder to offer advanced-track options that skip scaffolding. - **Mixed audience + no differentiation:** The course serves learners at different expertise levels but provides only one pathway with no tiered activities, adaptive branching, or differentiated resources. Flag as warning. - Fix: run /needs-analysis to establish a detailed learner profile, then - run /course-builder to create differentiated pathways. + Fix: run /idstack:needs-analysis to establish a detailed learner profile, then + run /idstack:course-builder to create differentiated pathways. - **Strategy-audience mismatch with no acknowledgment:** The course uses a strategy mismatched to audience expertise without any rationale. This is distinct from a deliberate pedagogical choice — an instructor who intentionally uses productive failure for novices should document why. Undocumented mismatches are flags. Flag as info. - Fix: run /course-builder to add instructor rationale or adjust strategy. + Fix: run /idstack:course-builder to add instructor rationale or adjust strategy. --- @@ -775,9 +792,9 @@ Present the **Top 3 fixes for maximum impact**: ### Top 3 Quick Wins | # | Finding | Impact | Skill to Run | |---|---------|--------|--------------| -| 1 | [finding] | [score] (T?/sev/ease) | /skill-name | -| 2 | [finding] | [score] (T?/sev/ease) | /skill-name | -| 3 | [finding] | [score] (T?/sev/ease) | /skill-name | +| 1 | [finding] | [score] (T?/sev/ease) | /idstack: | +| 2 | [finding] | [score] (T?/sev/ease) | /idstack: | +| 3 | [finding] | [score] (T?/sev/ease) | /idstack: | ``` Estimate the ease of fix based on: S = a single skill run fixes it, M = requires @@ -844,9 +861,9 @@ Then present the detailed findings: ### Top 3 Quick Wins | # | Finding | Impact | Skill to Run | |---|---------|--------|--------------| -| 1 | [finding] | [score] (T?/sev/ease) | /skill-name | -| 2 | [finding] | [score] (T?/sev/ease) | /skill-name | -| 3 | [finding] | [score] (T?/sev/ease) | /skill-name | +| 1 | [finding] | [score] (T?/sev/ease) | /idstack: | +| 2 | [finding] | [score] (T?/sev/ease) | /idstack: | +| 3 | [finding] | [score] (T?/sev/ease) | /idstack: | ``` ### Scoring Rubric @@ -868,38 +885,64 @@ Calculate the overall score from these components (total: 100 points): When recommending fixes, point users to the appropriate idstack skill: -- Misaligned or weak ILOs: "Run `/learning-objectives` to realign ILO-3 with +- Misaligned or weak ILOs: "Run `/idstack:learning-objectives` to realign ILO-3 with its assessment." -- Missing learner profile: "Run `/needs-analysis` to establish the learner +- Missing learner profile: "Run `/idstack:needs-analysis` to establish the learner profile that is currently missing." -- No task analysis: "Run `/needs-analysis` — the task analysis will inform +- No task analysis: "Run `/idstack:needs-analysis` — the task analysis will inform which activities are core vs. reference." -- Weak alignment chain: "Run `/learning-objectives` to rebuild the alignment +- Weak alignment chain: "Run `/idstack:learning-objectives` to rebuild the alignment matrix from your task analysis." --- ## Write Manifest -After completing the review, save results to the project manifest. +After completing the review, save results via the section-scoped merge tool — it +validates the section name against the canonical schema, preserves every other +section verbatim, bumps the top-level `updated` timestamp, and writes atomically: + +```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done +# payload file holds ONLY the quality_review section object (shape below) +"$_IDSTACK/bin/idstack-manifest-merge" --section quality_review --payload .idstack/.quality_review-payload.json && rm -f .idstack/.quality_review-payload.json +``` + +Write the payload object to `.idstack/.quality_review-payload.json` with the +Write tool first, then run the merge. **If no manifest exists yet**, the merge +tool exits with an error (exit code 4, `manifest not found`) — in that case fall +back to creating `.idstack/project.json` with the Write tool, initializing ALL +sections from the schema (including `context`, `needs_analysis`, and +`learning_objectives`) with empty/default values and placing your +`quality_review` data inside it. **CRITICAL — Manifest Integrity Rules:** -1. If a manifest already exists, READ it first with the Read tool. -2. Modify ONLY the `quality_review` section. Preserve all other sections - unchanged — `context`, `needs_analysis`, `learning_objectives`, and any - other sections must remain exactly as they were. -3. Before writing, verify the JSON is valid: matching braces, proper commas, - quoted strings, no trailing commas. -4. Update the top-level `updated` timestamp to reflect the current time. -5. If this is a new manifest, initialize ALL sections (including `context`, - `needs_analysis`, and `learning_objectives`) with empty/default values so - downstream skills find the expected structure. - -Populate the `quality_review` section with: +1. The payload must be valid JSON: matching braces, proper commas, quoted + strings, no trailing commas. The merge tool rejects malformed payloads + (exit code 1). +2. Never write secrets or credentials into the manifest. + +Payload shape — the file contains ONLY the section object, no `"quality_review"` wrapper: ```json { - "quality_review": { "report_path": "/course-quality-review.html>", "last_reviewed": "ISO-8601 timestamp", "qm_standards": { @@ -938,7 +981,7 @@ Populate the `quality_review` section with: "evidence_tier": "T1-T5", "severity": "critical|warning|info", "ease": "S|M|L", - "fix_skill": "/skill-name" + "fix_skill": "/idstack:" } ], "recommendations": [ @@ -949,13 +992,12 @@ Populate the `quality_review` section with: "fix": "..." } ] - } } ``` -When writing the manifest: +When building the payload: - Populate ALL fields in the `quality_review` section from the analysis above. -- Update the top-level `updated` timestamp to reflect the current time. +- The merge tool updates the top-level `updated` timestamp automatically. ### Score Trending Display @@ -975,6 +1017,24 @@ One source of truth per data point. After writing the manifest, generate a shareable HTML report. The report follows the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -1033,7 +1093,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -1044,10 +1104,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1424,6 +1506,24 @@ Include the overall_score so the preamble's context recovery can display score t across sessions (e.g., "Quality score trend: 62 -> 72 -> 78 over 3 reviews"). ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"course-quality-review","event":"completed","score":OVERALL_SCORE,"dimensions":{"teaching_presence":TP,"social_presence":SP,"cognitive_presence":CP}}' ``` @@ -1434,5 +1534,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"course-quality-review","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/dist/codex/skills/idstack-learn/SKILL.md b/dist/codex/skills/idstack-learn/SKILL.md index 6366a5b..3b6d61f 100644 --- a/dist/codex/skills/idstack-learn/SKILL.md +++ b/dist/codex/skills/idstack-learn/SKILL.md @@ -40,23 +40,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -68,6 +71,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -137,6 +154,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -170,7 +201,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -178,9 +211,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -238,11 +273,28 @@ Parse the user's intent and map to one of these commands: ### list (default) -Show the most recent learnings. If the user just said `/learn` with no arguments, +Show the most recent learnings. If the user just said `/idstack:learn` (Codex: `$learn`) with no arguments, this is the default. ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-search" --limit 10 ``` @@ -260,13 +312,47 @@ bloom-verbs | pedagogical | Avoid "understand" as ILO verb | 9/10 Search learnings by keyword. Supports `--cross-project` to include global learnings. ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-search" --keyword KEYWORD --limit 10 ``` For cross-project search: ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-search" --keyword KEYWORD --cross-project --limit 10 ``` @@ -277,7 +363,24 @@ If results include global learnings (tagged with `_source`), show their source p Delete a learning by its key. Always confirm with the user before deleting. ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-delete" KEY ``` @@ -286,7 +389,24 @@ for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" Copy a local learning to the global store so it's available across projects. ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-promote" KEY ``` @@ -295,7 +415,24 @@ for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" Export all learnings to a markdown file. ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-search" --limit 1000 ``` diff --git a/dist/codex/skills/idstack-learning-objectives/SKILL.md b/dist/codex/skills/idstack-learning-objectives/SKILL.md index a5adc57..215c0c6 100644 --- a/dist/codex/skills/idstack-learning-objectives/SKILL.md +++ b/dist/codex/skills/idstack-learning-objectives/SKILL.md @@ -41,23 +41,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -69,6 +72,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -138,6 +155,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -171,7 +202,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -179,9 +212,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -244,7 +279,7 @@ Key findings encoded as decision rules in this skill: - **Constructive alignment improves student outcomes.** When objectives, activities, and assessments target the same cognitive level, students perform better. Misalignment is - one of the most common and most fixable problems in course design [Alignment-1] + one of the most common and most fixable problems in course design [Alignment-1] [T5] [Alignment-10] [T2]. - **Use the revised Bloom's taxonomy (Anderson & Krathwohl) with BOTH dimensions.** @@ -278,33 +313,17 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest +## Manifest Inputs (skill-specific) -Before starting objective development, check for an existing project manifest. - -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` - -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to the - user, offer to fix it, and STOP until it is valid. Never silently overwrite corrupt JSON. -- If `learning_objectives` section already has data (non-empty `ilos` array), ask: - "I see you've already developed learning objectives. Want to update them or start fresh?" -- Preserve all existing sections when writing back. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. **If NO_MANIFEST:** -- Say: "I notice you haven't run `/needs-analysis` yet. Running it first gives me your +- Say: "I notice you haven't run `/idstack:needs-analysis` yet. Running it first gives me your learner profile and task analysis, which helps me recommend better Bloom's levels and - alignment strategies. Want to continue anyway, or run `/needs-analysis` first?" + alignment strategies. Want to continue anyway, or run `/idstack:needs-analysis` first?" - If the user wants to continue, proceed without manifest context. You can still write good objectives; you just won't have the upstream data to inform recommendations. -- You will create the manifest at the end of this skill's workflow. --- @@ -448,7 +467,7 @@ Record any flags in the `expertise_reversal_flags` array for the manifest. This is the core value of this skill. Constructive alignment means every ILO connects to both a learning activity AND an assessment, and all three target the same cognitive level -[Alignment-1] [Alignment-10] [T2]. +[Alignment-1] [T5] [Alignment-10] [T2]. ### Forward Pass: ILO to Activity @@ -541,6 +560,24 @@ Then list: Before writing the manifest, generate an HTML report so the designer has a single document to read. The report follows the **visual contract** in `templates/report.html.tmpl` (the skeleton) and the **content contract** in `templates/report-format.md` (severity ordering, citation format, what each placeholder must carry). ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -558,6 +595,7 @@ Write the HTML report at the path printed above (`.idstack/exports/ - **Summary:** 2–3 sentences — how many ILOs you have, how many are well-aligned, the single most important gap or mismatch the designer should know about. - **Skill-specific section before Findings** — add a `
` with `

Alignment table

` and an HTML `` (columns: ID, Objective, Knowledge, Process, Activity, Assessment, Alignment). Alignment values: `aligned` / `MISMATCH` / `GAP`. - **Finding ids:** `align-1`, `bloom-1`, `expertise-1`, etc. Findings come from bidirectional alignment gaps, Bloom's-level mismatches, expertise-reversal flags, and ambiguous verbs that were clarified. +- **Top recommendations:** the 3-5 highest-impact alignment fixes, ordered by leverage; cite each ([Domain-N] [TN]) and reference the finding id it addresses. - **Limitations:** alignment is read from manifest descriptions, not from the actual rubric criteria; expertise-reversal flags are inferred from the learner profile without a learner survey. - **Next steps:** Run `/idstack:assessment-design` to design assessments aligned to these objectives with evidence-based rubrics and feedback strategies. @@ -569,17 +607,58 @@ Every finding in the HTML must correspond to an entry in `learning_objectives.al Create or update the project manifest at `.idstack/project.json`. -**CRITICAL — Manifest Integrity Rules:** -1. If a manifest already exists, READ it first, then modify ONLY the `learning_objectives` - section. Preserve all other sections unchanged. -2. Include the COMPLETE schema structure. Do not omit fields. -3. Before writing, mentally verify the JSON is valid: matching braces, proper commas, - quoted strings, no trailing commas. -4. The `updated` timestamp must reflect the current time. -5. Set `learning_objectives.report_path` to the value of `$_REPORT_PATH` from the bash block above — i.e., `.idstack/exports//learning-objectives.html`. -6. If this is a new manifest (no needs analysis was run), initialize ALL sections - (including `needs_analysis`, `context`, and `quality_review`) with empty/default - values so downstream skills find the expected structure. +**If MANIFEST_EXISTS (primary path):** write the `learning_objectives` section via +`bin/idstack-manifest-merge`. The merge tool replaces only the named section, preserves +every other section verbatim, validates JSON, writes atomically (tempfile + rename), and +updates the top-level `updated` timestamp for you: + +```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done +"$_IDSTACK/bin/idstack-manifest-merge" --section learning_objectives --payload - <<'PAYLOAD' +{ + "report_path": "/learning-objectives.html>", + "ilos": [], + "alignment_matrix": { + "ilo_to_activity": {}, + "ilo_to_assessment": {}, + "gaps": [] + }, + "expertise_reversal_flags": [] +} +PAYLOAD +``` + +Replace the placeholder payload above with the actual session data. Include the COMPLETE +section structure — do not omit fields. If the tool exits non-zero (e.g., exit 4 = manifest +not found, exit 2 = malformed manifest), report the error to the user and stop; never +silently overwrite. + +**If NO_MANIFEST (first run only):** use the Write tool to create the full manifest — +first-run init writes the whole document; the merge tool replaces exactly one section per +call. Initialize ALL sections (including `needs_analysis`, `context`, and `quality_review`) +with empty/default values per the schema below so downstream skills find the expected +structure, set the `updated` timestamp to the current time, and verify the JSON is valid +before writing. + +In both paths, set `learning_objectives.report_path` to the value of `$_REPORT_PATH` from +the bash block above — i.e., `.idstack/exports//learning-objectives.html`. **Populate the `learning_objectives` section:** @@ -606,7 +685,7 @@ Write the manifest, then confirm to the user: read. Open it in any browser; the folder is self-contained. - System state: `.idstack/project.json` (the manifest — for downstream skills). -**Next step:** Run `/assessment-design` to design assessments aligned to your objectives +**Next step:** Run `/idstack:assessment-design` to design assessments aligned to your objectives with evidence-based rubrics and feedback strategies." --- @@ -624,7 +703,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -635,10 +714,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1013,6 +1114,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"learning-objectives","event":"completed"}' ``` @@ -1023,5 +1142,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"learning-objectives","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/dist/codex/skills/idstack-needs-analysis/SKILL.md b/dist/codex/skills/idstack-needs-analysis/SKILL.md index f659b8a..ed7ef86 100644 --- a/dist/codex/skills/idstack-needs-analysis/SKILL.md +++ b/dist/codex/skills/idstack-needs-analysis/SKILL.md @@ -41,23 +41,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -69,6 +72,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -138,6 +155,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -171,7 +202,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -179,9 +212,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -262,28 +297,10 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest - -Before starting the needs assessment, check for an existing project manifest. - -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` - -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to the - user, offer to fix it, and STOP until it is valid. Never silently overwrite corrupt JSON. -- If `needs_analysis` section already has data, ask: "I see you've already run a needs - analysis. Want to update it or start fresh?" -- Preserve all existing sections when writing back. +## Manifest Inputs (skill-specific) -**If NO_MANIFEST:** -- You will create the manifest at the end of this skill's workflow. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. --- @@ -545,6 +562,24 @@ Based on the learner profile, note which instructional strategies are appropriat Generate an HTML report so the designer has a single document to read. The report follows the **visual contract** in `templates/report.html.tmpl` (the skeleton) and the **content contract** in `templates/report-format.md` (severity ordering, citation format, what each placeholder must carry). ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -575,6 +610,8 @@ Every finding in the HTML must correspond to a finding the manifest's `needs_ana Create or update the project manifest. Use the Write tool to write `.idstack/project.json`. +**Why this skill uses the Write-tool fallback (not `bin/idstack-manifest-merge`):** needs-analysis writes top-level fields (`project_name`, `created`), the `context` section, AND the `needs_analysis` section — and on a first run it initializes ALL sections so downstream skills find the expected structure. The merge tool replaces exactly one section per call, so this skill uses the Read-modify-Write path described in `templates/manifest-schema.md`. When a manifest already exists, preserve every other section unchanged. + **CRITICAL — Manifest Integrity Rules:** 1. If a manifest already exists, READ it first, then modify only the sections this skill owns (context, needs_analysis). Preserve all other sections unchanged. @@ -597,7 +634,7 @@ Write the manifest, then confirm to the user: (CSS is bundled), so you can zip or email it. - System state: `.idstack/project.json` (the manifest — for downstream skills). -**Next step:** Run `/learning-objectives` to develop learning objectives based on +**Next step:** Run `/idstack:learning-objectives` to develop learning objectives based on this analysis. The objectives skill will read your task analysis and learner profile to recommend appropriate Bloom's levels and alignment strategies." @@ -616,7 +653,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -627,10 +664,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1005,6 +1064,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"needs-analysis","event":"completed"}' ``` @@ -1015,5 +1092,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"needs-analysis","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/dist/codex/skills/idstack-pipeline/SKILL.md b/dist/codex/skills/idstack-pipeline/SKILL.md index 9f8cac6..86ed5ef 100644 --- a/dist/codex/skills/idstack-pipeline/SKILL.md +++ b/dist/codex/skills/idstack-pipeline/SKILL.md @@ -40,23 +40,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -68,6 +71,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -137,6 +154,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -170,7 +201,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -178,9 +211,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -235,19 +270,19 @@ skipping skills that have already been completed. The canonical pipeline order is: ``` -1. /needs-analysis — Three-level needs assessment -2. /learning-objectives — Evidence-based ILO development -3. /assessment-design — Assessment & rubric design -4. /course-builder — Generate course content -5. /course-quality-review — Quality audit (QM + CoI) -6. /accessibility-review — WCAG + UDL review -7. /red-team — Adversarial audit -8. /course-export — Package for LMS +1. /idstack:needs-analysis — Three-level needs assessment +2. /idstack:learning-objectives — Evidence-based ILO development +3. /idstack:assessment-design — Assessment & rubric design +4. /idstack:course-builder — Generate course content +5. /idstack:course-quality-review — Quality audit (QM + CoI) +6. /idstack:accessibility-review — WCAG + UDL review +7. /idstack:red-team — Adversarial audit +8. /idstack:course-export — Package for LMS ``` -**Alternative entry point:** If the user has run `/course-import` (visible in timeline), -the pipeline starts at `/learning-objectives` (skipping /needs-analysis, since the import -populated the manifest with equivalent data). +**Alternative entry point:** If the user has run `/idstack:course-import` (visible in +timeline), the pipeline starts at `/idstack:learning-objectives` (skipping +`/idstack:needs-analysis`, since the import populated the manifest with equivalent data). ## Determining Completed Skills @@ -304,17 +339,17 @@ Show the user a status table before starting: ``` Pipeline Status: - [done] /needs-analysis - [done] /learning-objectives - [next] /assessment-design <-- starting here - [ ] /course-builder - [ ] /course-quality-review - [ ] /accessibility-review - [ ] /red-team - [ ] /course-export + [done] /idstack:needs-analysis + [done] /idstack:learning-objectives + [next] /idstack:assessment-design <-- starting here + [ ] /idstack:course-builder + [ ] /idstack:course-quality-review + [ ] /idstack:accessibility-review + [ ] /idstack:red-team + [ ] /idstack:course-export ``` -Ask: "Ready to continue the pipeline from /assessment-design?" (using AskUserQuestion +Ask: "Ready to continue the pipeline from /idstack:assessment-design?" (using AskUserQuestion with options: "Yes, continue" / "Start from a different skill" / "Re-run a completed skill") If the user picks "Start from a different skill" or "Re-run a completed skill", @@ -324,8 +359,10 @@ ask which one using AskUserQuestion with the skill list as options. For each skill from the starting point onward: -1. Announce: "Starting /skill-name..." -2. Invoke the skill using the `Skill` tool with the skill name (e.g., `skill: "needs-analysis"`) +1. Announce: "Starting /idstack:..." +2. Invoke the skill using the `Skill` tool with the **plugin-namespaced** skill name + (e.g., `skill: "idstack:needs-analysis"`). Plugin skills are addressed as + `plugin:skill` — a bare `"needs-analysis"` does not resolve in Claude Code. 3. The skill will run its full workflow including all AskUserQuestion interactions 4. When the skill completes (logs to timeline), **execute Step 4 (Generate Course Dashboard) inline** to refresh `.idstack/exports//index.html` against the new per-skill report. This keeps the dashboard fresh if the designer pauses partway through. 5. Announce completion and move to next. @@ -339,7 +376,7 @@ reading the timeline and continuing from the new starting point. Generate the co dashboard (Step 4) before stopping so the partial-run dashboard is up to date. **Between skills**, briefly announce the transition: -"[skill-name] complete. Course dashboard refreshed. Moving to /next-skill..." +"[skill-name] complete. Course dashboard refreshed. Moving to /idstack:..." ### Step 4: Generate Course Dashboard @@ -353,6 +390,24 @@ After each skill completes (or after the orchestrator finishes the run, includin **Prep.** Compute the course slug and prepare the export folder: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") _EXPORT_DIR=".idstack/exports/$_SLUG" @@ -393,8 +448,31 @@ The dashboard is overwritten on every pipeline run — historical dashboards are When all remaining skills have been executed: - Announce: "Pipeline complete. Your course has been through all 8 stages." - Confirm the dashboard path: "Course dashboard at `.idstack/exports//index.html` — open it in any browser for the cross-cutting view; the per-skill HTML reports in the same folder carry the full detail. Zip the folder to hand it to a stakeholder." -- If `/course-quality-review` produced a score, show it. +- If `/idstack:course-quality-review` produced a score, show it. - Remind the user they can re-run any skill individually if needed, and that re-running `/idstack:pipeline` regenerates the course dashboard. +- Log the pipeline run so "the pipeline was run" is recoverable from the timeline: + +```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done +[ -n "$_IDSTACK" ] && "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"pipeline","event":"completed"}' +``` ## Important Rules @@ -404,6 +482,6 @@ When all remaining skills have been executed: questions or commentary between the skill's AskUserQuestion prompts. - **Respect the user.** If at any point the user says "stop", "pause", or "that's enough for now", stop the pipeline gracefully. Their progress is saved in - timeline.jsonl and they can resume later with `/idstack pipeline`. + timeline.jsonl and they can resume later with `/idstack:pipeline`. - **No quality gate yet.** In v2.0, skip logic is purely completion-based. Quality-gated skipping (only skip if score > threshold) is planned for a future version. diff --git a/dist/codex/skills/idstack-red-team/SKILL.md b/dist/codex/skills/idstack-red-team/SKILL.md index 735daeb..bc0067f 100644 --- a/dist/codex/skills/idstack-red-team/SKILL.md +++ b/dist/codex/skills/idstack-red-team/SKILL.md @@ -42,23 +42,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -70,6 +73,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -139,6 +156,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -172,7 +203,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -180,9 +213,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -270,7 +305,7 @@ No automatic re-verification. If the user wants to confirm fixes hold, they re-r The preamble above already ran the manifest check. Now confirm scope. **Determine course inputs:** -- If `MANIFEST_EXISTS`: the orchestrator will read all sections (`needs_analysis`, `learning_objectives`, `assessment_design`, `course_builder`, `quality_review`, `accessibility_review`). +- If `MANIFEST_EXISTS`: the orchestrator will read all sections (`needs_analysis`, `learning_objectives`, `assessments`, `course_content`, `quality_review`, `accessibility_review`). - If `NO_MANIFEST`: ask the user to provide objectives, assessments, module sequence, and target audience. Capture answers as a brief block to pass to the orchestrator. Standalone mode reduces precision on Dimensions 1 (alignment) and 5 (prerequisites). **Ask one focus question** via AskUserQuestion: @@ -292,7 +327,7 @@ Use the **Agent tool** with `subagent_type=general-purpose`. The prompt is the f - `{{FOCUS}}` → the user's choice from Step 1 (or `Full sweep` by default) - `{{MANIFEST_INFO}}` → either `Manifest at .idstack/project.json — read it directly.` or, in standalone mode, the captured course information from Step 1 -- `{{COURSE_FILES_HINT}}` → if the manifest has `course_builder.output_path`, set this to that path; otherwise `Look under ./course/ or ./modules/ for generated course files.` +- `{{COURSE_FILES_HINT}}` → if the manifest has `course_content.content_dir`, set this to that path; otherwise `Look under ./course/ or ./modules/ for generated course files.` Then call Agent. Block on its return. @@ -496,6 +531,24 @@ Contextualize: The orchestrator emits an HTML report. Follow the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -575,6 +628,24 @@ the orchestrator's HTML report at `$_REPORT_PATH` (the report is the source of t not re-derive from the orchestrator's return summary, which is lossy). ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload - <<'PAYLOAD' { "updated": "", @@ -605,8 +676,31 @@ Each finding object: `{"id": "alignment-1", "description": "...", "module": "Mod The merge tool exits non-zero (and prints a diagnostic on stderr) if the payload is malformed, the manifest is corrupt, or the section name is misspelled — never silently overwriting. If -`.idstack/project.json` doesn't exist yet, run `bin/idstack-migrate .idstack/project.json` -first (it creates a fresh canonical manifest). +`.idstack/project.json` doesn't exist yet (standalone mode), create it first with `--init`, +which writes a canonical manifest with every section at its default — without one the merge +tool exits 4: + +```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done +"$_IDSTACK/bin/idstack-migrate" --init .idstack/project.json >/dev/null +``` **Fallback (if `bin/idstack-manifest-merge` is unavailable):** Read the full manifest, modify only the `red_team_audit` section, Write back. Preserve all other sections verbatim. The @@ -631,6 +725,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"red-team","event":"completed"}' ``` @@ -639,5 +751,23 @@ Include skill-specific fields where available (confidence_score, focus, fixes_ap If you discover a non-obvious project-specific quirk during this session (LMS behavior, import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"red-team","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/docs/index.html b/docs/index.html index ddfa8b0..6cb7106 100644 --- a/docs/index.html +++ b/docs/index.html @@ -42,9 +42,9 @@ "url": "https://idstack.org", "applicationCategory": "EducationalApplication", "operatingSystem": "macOS, Linux, Windows", - "softwareVersion": "3.2.0", + "softwareVersion": "3.3.0", "datePublished": "2026-04-20", - "dateModified": "2026-05-14", + "dateModified": "2026-08-04", "offers": { "@type": "Offer", "price": "0", @@ -926,7 +926,7 @@ MIT - v3.2.0.0 — Marketplace install + v3.3.0.0 — Course memory fixed

Evidence-based instructional design, transparently cited.

108 peer-reviewed studies across 11 research domains. Every recommendation tagged with its evidence tier — T1 meta-analyses and RCTs through T5 expert opinion. Runs in Claude Code, OpenAI Codex CLI, and (soon) Gemini.

@@ -1172,14 +1172,14 @@

Install in about five minutes.

What's new

-

v3.2.0.0 — Install through the Claude Code plugin marketplace.

+

v3.3.0.0 — Course memory that actually remembers.

-

May 14, 2026 · v3.2.0.0 · latest

-

The install Claude Code actually discovers.

-

./setup now registers idstack as a Claude Code plugin marketplace and installs it from there. Recent Claude Code versions stopped discovering plugins from the bare symlink the old setup created, so /idstack:<skill> commands silently never appeared in the slash picker. If that happened to you, pull the latest and re-run ./setup.

-

A fresh install is one line: git clone https://github.com/savvides/idstack.git && cd idstack && ./setup. Setup is idempotent, auto-detects Claude Code and Codex CLI on your PATH, and clears out installs from older versions. Restart Claude Code afterward — plugins load at session start.

+

August 4, 2026 · v3.3.0.0 · latest

+

Welcome-back messages work on stock macOS now.

+

On any Python older than 3.12 — including the 3.9 that macOS ships — the session-memory code inside every skill hit a silent syntax error. No welcome-back summary, no quality-score trend, no suggested next step. Fixed, and now exercised on Python 3.9 in CI so it stays fixed.

+

Also in this release: /idstack:pipeline invokes its child skills correctly in Claude Code, skills notice previous runs again ("update the results or start fresh?"), imported courses get a proper next-step suggestion, and a course name with an apostrophe no longer blanks the status dashboard. To update: cd into your idstack clone, then git pull && ./setup.

→ All releases on GitHub

diff --git a/setup b/setup index ed698ee..76c642b 100755 --- a/setup +++ b/setup @@ -7,11 +7,16 @@ set -e # the new plugin symlink pointing at a deleted target. IDSTACK_DIR="$(cd "$(dirname "$0")" && pwd -P)" +# Shared legacy-VERSION classifier (also sourced by bin/idstack-doctor; pinned +# by test/test-version-classifier.sh). +. "$IDSTACK_DIR/bin/lib/version-classify.sh" + # Default install: user-scope plugin so /idstack: is discovered. LEGACY_SKILLS_DIR="$HOME/.claude/skills" SCOPE="global" CLAUDE_SCOPE="user" # claude plugin scope: user (global) or project (--local) CODEX_OPT_IN="" # "" = auto-detect, "yes" = force-install, "no" = skip +KEEP_LEGACY=0 # --keep-legacy: leave every legacy install shape in place for arg in "$@"; do case "$arg" in @@ -22,13 +27,15 @@ for arg in "$@"; do ;; --codex) CODEX_OPT_IN="yes" ;; --no-codex) CODEX_OPT_IN="no" ;; - --keep-legacy) ;; # consumed later in the legacy-cleanup branch + --keep-legacy) KEEP_LEGACY=1 ;; -h|--help) echo "Usage: ./setup [--local] [--codex|--no-codex] [--keep-legacy]" echo " --local install at project scope (./.claude/) instead of user scope" echo " --codex force Codex install even if codex is not on PATH" echo " --no-codex skip Codex install even if codex is on PATH" - echo " --keep-legacy keep pre-v2 install at \$HOME/.claude/skills/idstack (default: remove)" + echo " --keep-legacy keep legacy installs in place — the pre-v2.0.1.0 dispatcher" + echo " dir, the v2.0 dispatcher symlink, and pre-v2 per-skill" + echo " symlinks under \$HOME/.claude/skills/ (default: remove)" exit 0 ;; *) echo "ERROR: unknown argument: $arg" >&2; echo "Run ./setup --help for usage." >&2; exit 2 ;; @@ -68,15 +75,12 @@ fi LEGACY_SKILLS_LINK="$LEGACY_SKILLS_DIR/idstack" if [ -L "$LEGACY_SKILLS_LINK" ]; then legacy_target=$(readlink "$LEGACY_SKILLS_LINK" 2>/dev/null || true) - legacy_resolved=$(cd "$LEGACY_SKILLS_LINK" 2>/dev/null && pwd -P || true) - if [ "$legacy_resolved" = "$IDSTACK_DIR" ]; then - # The legacy symlink points at the same physical repo we're installing - # from. Removing it is fine — the new plugin symlink replaces it. - rm "$LEGACY_SKILLS_LINK" - echo " removed legacy symlink: $LEGACY_SKILLS_LINK (was -> $legacy_target)" + if [ "$KEEP_LEGACY" = "1" ]; then + echo " KEEPING legacy symlink: $LEGACY_SKILLS_LINK (--keep-legacy)" + echo " NOTE: this shadows the plugin namespace; /idstack: may fail." else rm "$LEGACY_SKILLS_LINK" - echo " removed legacy symlink: $LEGACY_SKILLS_LINK" + echo " removed legacy symlink: $LEGACY_SKILLS_LINK${legacy_target:+ (was -> $legacy_target)}" fi elif [ -d "$LEGACY_SKILLS_LINK" ] && [ "$LEGACY_SKILLS_LINK" != "$IDSTACK_DIR" ]; then # Pre-v2.0.1.0 install was a real clone with a top-level dispatcher @@ -92,17 +96,14 @@ elif [ -d "$LEGACY_SKILLS_LINK" ] && [ "$LEGACY_SKILLS_LINK" != "$IDSTACK_DIR" ] legacy_is_old_version=0 if [ -f "$LEGACY_SKILLS_LINK/VERSION" ]; then legacy_version=$(tr -d '[:space:]' < "$LEGACY_SKILLS_LINK/VERSION") - # Two arms: explicitly skip modern/future versions first, then flag the - # legacy ones. 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 "$legacy_version" in - 2.0.[1-9]*|2.[1-9]*|[3-9]*|[1-9][0-9]*) ;; - 0.*|1.*|2.0.0.*|2.0.0) legacy_is_old_version=1 ;; - esac + # Classified by the shared classifier in bin/lib/version-classify.sh + # (pinned by test/test-version-classifier.sh). + if [ "$(classify_version "$legacy_version")" = "legacy" ]; then + legacy_is_old_version=1 + fi fi if [ "$legacy_is_dispatcher" = "1" ] || [ "$legacy_is_old_version" = "1" ]; then - if [[ " $* " == *" --keep-legacy "* ]]; then + if [ "$KEEP_LEGACY" = "1" ]; then echo " KEEPING legacy install dir: $LEGACY_SKILLS_LINK (--keep-legacy)" echo " NOTE: this will shadow the plugin namespace; /idstack: may fail." else @@ -123,8 +124,12 @@ for skill in $LEGACY_SKILLS; do if [ -L "$legacy" ]; then link_target=$(readlink "$legacy" 2>/dev/null || true) if echo "$link_target" | grep -q "idstack"; then - rm "$legacy" - echo " cleaned up legacy: $legacy" + if [ "$KEEP_LEGACY" = "1" ]; then + echo " KEEPING legacy symlink: $legacy (--keep-legacy)" + else + rm "$legacy" + echo " cleaned up legacy: $legacy" + fi fi fi done @@ -133,23 +138,39 @@ done # ~/.claude/plugins/idstack (or ./.claude/plugins/idstack for --local). Current # Claude Code discovers plugins through the marketplace registry, not bare # symlinks, so that link is dead weight. Only ever removes a *symlink* — a real -# directory there might be someone's clone, and is left untouched. -for plugins_base in "$HOME/.claude/plugins" "$(pwd)/.claude/plugins"; do - vestigial="$plugins_base/idstack" - if [ -L "$vestigial" ]; then - rm "$vestigial" - echo " removed vestigial symlink: $vestigial (pre-marketplace install method)" - fi -done +# directory there might be someone's clone, and is left untouched. Scope-aware: +# a --local install cleans only ./.claude/plugins and never mutates $HOME. +if [ "$SCOPE" = "local" ]; then + vestigial="$(pwd)/.claude/plugins/idstack" +else + vestigial="$HOME/.claude/plugins/idstack" +fi +if [ -L "$vestigial" ]; then + rm "$vestigial" + echo " removed vestigial symlink: $vestigial (pre-marketplace install method)" +fi # Claude Code install — register this repo as a single-plugin marketplace, then # install from it. Both commands are idempotent, so re-running setup is safe. # The marketplace source is this repo's path; `marketplace update` re-reads it, # and `plugin update` picks up a newer committed version on `git pull && ./setup`. if command -v claude >/dev/null 2>&1; then - claude plugin marketplace add "$IDSTACK_DIR" + # The two required calls fail loud with a manual-recovery hint — under + # `set -e` a bare nonzero exit here used to abort setup silently, before + # the Codex block, with no message at all. + if ! claude plugin marketplace add "$IDSTACK_DIR"; then + echo "" >&2 + echo "ERROR: 'claude plugin marketplace add' failed. Register manually, then re-run ./setup:" >&2 + echo " claude plugin marketplace add \"$IDSTACK_DIR\"" >&2 + exit 1 + fi claude plugin marketplace update idstack >/dev/null 2>&1 || true - claude plugin install idstack@idstack --scope "$CLAUDE_SCOPE" + if ! claude plugin install idstack@idstack --scope "$CLAUDE_SCOPE"; then + echo "" >&2 + echo "ERROR: 'claude plugin install' failed. Install manually:" >&2 + echo " claude plugin install idstack@idstack --scope $CLAUDE_SCOPE" >&2 + exit 1 + fi claude plugin update idstack@idstack --scope "$CLAUDE_SCOPE" >/dev/null 2>&1 || true echo "" echo "idstack installed (Claude Code) — scope: $CLAUDE_SCOPE." @@ -196,6 +217,7 @@ if [ "$CODEX_INSTALL" = "1" ]; then else # Part 1: per-skill symlinks for auto-discovery. mkdir -p "$CODEX_SKILLS_DIR" + linked_count=0 for skill_dir in "$CODEX_BUNDLE_SKILLS"/idstack-*; do [ -d "$skill_dir" ] || continue skill_name=$(basename "$skill_dir") @@ -209,8 +231,9 @@ if [ "$CODEX_INSTALL" = "1" ]; then rm -rf "$target" fi ln -snf "$skill_dir" "$target" + linked_count=$((linked_count + 1)) done - echo " linked 11 skills into: $CODEX_SKILLS_DIR/idstack-*" + echo " linked $linked_count skills into: $CODEX_SKILLS_DIR/idstack-*" # Part 2: whole-repo symlink so $_IDSTACK/bin/idstack-* resolves in skill bodies. mkdir -p "$CODEX_BIN_PARENT" diff --git a/skills/accessibility-review/SKILL.md b/skills/accessibility-review/SKILL.md index 69fb96b..e639706 100644 --- a/skills/accessibility-review/SKILL.md +++ b/skills/accessibility-review/SKILL.md @@ -50,23 +50,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -78,6 +81,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -147,6 +164,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -180,7 +211,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -188,9 +221,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -268,33 +303,17 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest +## Manifest Inputs (skill-specific) -Before starting the review, check for an existing project manifest. - -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to - the user, offer to fix it, and STOP until it is valid. Never silently overwrite - corrupt JSON. - Check which sections are populated. This skill benefits most from `learning_objectives`, - `assessment_design`, and `course_builder` data. -- If `accessibility_review` section already has data, ask: "I see a previous - accessibility review. Want to update it or start fresh?" -- Preserve all existing sections when writing back. - -**If NO_MANIFEST:** -- That is fine. This skill works standalone. Gather course information through - AskUserQuestion. You will create the manifest at the end if the user wants to - save results. + `assessments`, and `course_content` data. +- If no manifest: this skill works standalone. Gather course information through + AskUserQuestion. At the end, if the user wants the results saved, create the + manifest first (`bin/idstack-migrate --init`, see Write Manifest) — the merge + tool needs one to merge into. --- @@ -524,6 +543,24 @@ Calculate the accessibility score (0-100): Generate an HTML report so the designer has a single document covering both compliance (WCAG, the Must Fix layer) and inclusion (UDL, the Should Improve layer). The report follows the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -556,6 +593,24 @@ and atomically updates the top-level `updated` timestamp. The payload must inclu `report_path` set to the value of `$_REPORT_PATH` from the bash block above (e.g., `.idstack/exports//accessibility-review.html`). ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-manifest-merge" --section accessibility_review --payload - <<'PAYLOAD' { "updated": "", @@ -584,10 +639,35 @@ and atomically updates the top-level `updated` timestamp. The payload must inclu PAYLOAD ``` -If `.idstack/project.json` doesn't exist yet, run `bin/idstack-migrate .idstack/project.json` -first — that creates a fresh canonical manifest. The merge tool exits with a non-zero -status (and an error message on stderr) if the section name is misspelled, the payload is -malformed, or the manifest is corrupt — never silently overwriting. +If `.idstack/project.json` doesn't exist yet — the standalone case this skill explicitly +supports — create it first with `--init`, which writes a canonical manifest with every +section at its default. Without one the merge tool exits 4: + +```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done +"$_IDSTACK/bin/idstack-migrate" --init .idstack/project.json >/dev/null +``` + +The merge tool exits with a non-zero status (and an error message on stderr) if the +section name is misspelled, the payload is malformed, or the manifest is corrupt — +never silently overwriting. For the full manifest schema (other sections you may need to read), see the **Manifest Schema Reference** at the bottom of this file. @@ -628,7 +708,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -639,10 +719,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1017,6 +1119,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"accessibility-review","event":"completed"}' ``` @@ -1027,5 +1147,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"accessibility-review","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/accessibility-review/SKILL.md.tmpl b/skills/accessibility-review/SKILL.md.tmpl index 5b934b5..d2aa89a 100644 --- a/skills/accessibility-review/SKILL.md.tmpl +++ b/skills/accessibility-review/SKILL.md.tmpl @@ -52,33 +52,17 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest +## Manifest Inputs (skill-specific) -Before starting the review, check for an existing project manifest. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` - -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to - the user, offer to fix it, and STOP until it is valid. Never silently overwrite - corrupt JSON. - Check which sections are populated. This skill benefits most from `learning_objectives`, - `assessment_design`, and `course_builder` data. -- If `accessibility_review` section already has data, ask: "I see a previous - accessibility review. Want to update it or start fresh?" -- Preserve all existing sections when writing back. - -**If NO_MANIFEST:** -- That is fine. This skill works standalone. Gather course information through - AskUserQuestion. You will create the manifest at the end if the user wants to - save results. + `assessments`, and `course_content` data. +- If no manifest: this skill works standalone. Gather course information through + AskUserQuestion. At the end, if the user wants the results saved, create the + manifest first (`bin/idstack-migrate --init`, see Write Manifest) — the merge + tool needs one to merge into. --- @@ -308,6 +292,7 @@ Calculate the accessibility score (0-100): Generate an HTML report so the designer has a single document covering both compliance (WCAG, the Must Fix layer) and inclusion (UDL, the Should Improve layer). The report follows the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +{{IDSTACK_RESOLVE}} # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -340,6 +325,7 @@ and atomically updates the top-level `updated` timestamp. The payload must inclu `report_path` set to the value of `$_REPORT_PATH` from the bash block above (e.g., `.idstack/exports//accessibility-review.html`). ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-manifest-merge" --section accessibility_review --payload - <<'PAYLOAD' { "updated": "", @@ -368,10 +354,18 @@ and atomically updates the top-level `updated` timestamp. The payload must inclu PAYLOAD ``` -If `.idstack/project.json` doesn't exist yet, run `bin/idstack-migrate .idstack/project.json` -first — that creates a fresh canonical manifest. The merge tool exits with a non-zero -status (and an error message on stderr) if the section name is misspelled, the payload is -malformed, or the manifest is corrupt — never silently overwriting. +If `.idstack/project.json` doesn't exist yet — the standalone case this skill explicitly +supports — create it first with `--init`, which writes a canonical manifest with every +section at its default. Without one the merge tool exits 4: + +```bash +{{IDSTACK_RESOLVE}} +"$_IDSTACK/bin/idstack-migrate" --init .idstack/project.json >/dev/null +``` + +The merge tool exits with a non-zero status (and an error message on stderr) if the +section name is misspelled, the payload is malformed, or the manifest is corrupt — +never silently overwriting. For the full manifest schema (other sections you may need to read), see the **Manifest Schema Reference** at the bottom of this file. @@ -412,6 +406,7 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"accessibility-review","event":"completed"}' ``` @@ -422,5 +417,6 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"accessibility-review","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/assessment-design/SKILL.md b/skills/assessment-design/SKILL.md index a413ac1..909465a 100644 --- a/skills/assessment-design/SKILL.md +++ b/skills/assessment-design/SKILL.md @@ -49,23 +49,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -77,6 +80,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -146,6 +163,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -179,7 +210,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -187,9 +220,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -233,7 +268,7 @@ Example: "Reminder: this Canvas instance uses custom rubric formatting (discover --- -**Skill-specific manifest check:** If the manifest `assessment_design` section already has data, +**Skill-specific manifest check:** If the manifest `assessments` section already has data, ask the user: "I see you've already run this skill. Want to update the results or start fresh?" # Assessment Design — Rubrics, Feedback Strategies & Formative Checkpoints @@ -322,34 +357,20 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest - -Before starting assessment design, check for an existing project manifest. - -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` +## Manifest Inputs (skill-specific) -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to the - user, offer to fix it, and STOP until it is valid. Never silently overwrite corrupt JSON. -- If `assessments` section already has data (non-empty `items` array), ask: - "I see you've already designed assessments. Want to update them or start fresh?" -- Preserve all existing sections when writing back. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. **If NO_MANIFEST:** -- Say: "I see you haven't run `/learning-objectives` yet. Running it first gives me +- Say: "I see you haven't run `/idstack:learning-objectives` yet. Running it first gives me your ILOs with Bloom's classifications, which helps me recommend assessment types that actually measure your stated outcomes. Want to continue anyway, or run - `/learning-objectives` first?" + `/idstack:learning-objectives` first?" - If the user wants to continue, proceed without manifest context. You can still - design assessments; you just won't have the upstream alignment data. -- You will create the manifest at the end of this skill's workflow. + design assessments; you just won't have the upstream alignment data. The manifest + gets created at the end so results persist (`bin/idstack-migrate --init`, see + Write Manifest) — the merge tool needs one to merge into. --- @@ -390,7 +411,7 @@ assessments to match."** For each objective provided, classify on both Bloom's dimensions (knowledge and cognitive process) before proceeding to assessment design. Use the same classification -approach as the `/learning-objectives` skill: ask for clarification when verbs are +approach as the `/idstack:learning-objectives` skill: ask for clarification when verbs are ambiguous [Alignment-12] [T2]. ### Mode 3: Audit Existing Assessments @@ -584,9 +605,9 @@ and close performance gaps before they matter [Assessment-9] [T5]. - **Feedback type:** Immediate and elaborated whenever possible. Automated feedback is acceptable for remember/understand levels. Higher levels need human or structured peer feedback [Assessment-10] [T1]. -- **Purpose:** Close the gap between current and desired performance [Assessment-9] - [T5]. Each checkpoint should give students evidence of where they stand relative - to the rubric criteria. +- **Purpose:** Close the gap between current and desired performance + [Assessment-9] [T5]. Each checkpoint should give students evidence of where they + stand relative to the rubric criteria. **Student-initiated formative assessment:** Where possible, design checkpoints that students can initiate on their own (practice @@ -666,6 +687,24 @@ Consider adding a formative peer review checkpoint to partially address the gap. Before writing the manifest, generate an HTML report so the designer has a single document to read. The report follows the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -706,6 +745,24 @@ first, merge in your changes, then pass the full updated section as the payload. The `assessments` payload must include `report_path` set to the value of `$_REPORT_PATH` from the bash block above (e.g., `.idstack/exports//assessment-design.html`). ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Section 1: assessments "$_IDSTACK/bin/idstack-manifest-merge" --section assessments --payload - <<'PAYLOAD' @@ -721,7 +778,29 @@ PAYLOAD If `bin/idstack-manifest-merge` is unavailable: fall back to manual write (Read manifest, modify only the two sections, Write back, preserve all others). -If `.idstack/project.json` does not exist yet, run `bin/idstack-migrate .idstack/project.json` first — that creates a fresh canonical manifest. The merge tool then merges into it. +If `.idstack/project.json` does not exist yet — the standalone case, where the user chose to continue without upstream data — create it first with `--init`, which writes a canonical manifest with every section at its default. The merge tool then merges into it (without a manifest it exits 4): + +```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done +"$_IDSTACK/bin/idstack-migrate" --init .idstack/project.json >/dev/null +``` **Populate the `assessments` section:** @@ -806,7 +885,7 @@ Write the manifest, then confirm to the user: alignment notes. Open it in any browser; the folder is self-contained. - System state: `.idstack/project.json` (the manifest — for downstream skills). -**Next step:** Run `/course-builder` to generate the full course content including +**Next step:** Run `/idstack:course-builder` to generate the full course content including assessment documents, rubric handouts, and assignment instructions." --- @@ -824,7 +903,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -835,10 +914,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1213,6 +1314,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"assessment-design","event":"completed"}' ``` @@ -1223,5 +1342,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"assessment-design","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/assessment-design/SKILL.md.tmpl b/skills/assessment-design/SKILL.md.tmpl index 08f1349..a83056c 100644 --- a/skills/assessment-design/SKILL.md.tmpl +++ b/skills/assessment-design/SKILL.md.tmpl @@ -17,7 +17,7 @@ allowed-tools: {{PREAMBLE}} -**Skill-specific manifest check:** If the manifest `assessment_design` section already has data, +**Skill-specific manifest check:** If the manifest `assessments` section already has data, ask the user: "I see you've already run this skill. Want to update the results or start fresh?" # Assessment Design — Rubrics, Feedback Strategies & Formative Checkpoints @@ -106,34 +106,20 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest +## Manifest Inputs (skill-specific) -Before starting assessment design, check for an existing project manifest. - -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` - -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to the - user, offer to fix it, and STOP until it is valid. Never silently overwrite corrupt JSON. -- If `assessments` section already has data (non-empty `items` array), ask: - "I see you've already designed assessments. Want to update them or start fresh?" -- Preserve all existing sections when writing back. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. **If NO_MANIFEST:** -- Say: "I see you haven't run `/learning-objectives` yet. Running it first gives me +- Say: "I see you haven't run `/idstack:learning-objectives` yet. Running it first gives me your ILOs with Bloom's classifications, which helps me recommend assessment types that actually measure your stated outcomes. Want to continue anyway, or run - `/learning-objectives` first?" + `/idstack:learning-objectives` first?" - If the user wants to continue, proceed without manifest context. You can still - design assessments; you just won't have the upstream alignment data. -- You will create the manifest at the end of this skill's workflow. + design assessments; you just won't have the upstream alignment data. The manifest + gets created at the end so results persist (`bin/idstack-migrate --init`, see + Write Manifest) — the merge tool needs one to merge into. --- @@ -174,7 +160,7 @@ assessments to match."** For each objective provided, classify on both Bloom's dimensions (knowledge and cognitive process) before proceeding to assessment design. Use the same classification -approach as the `/learning-objectives` skill: ask for clarification when verbs are +approach as the `/idstack:learning-objectives` skill: ask for clarification when verbs are ambiguous [Alignment-12] [T2]. ### Mode 3: Audit Existing Assessments @@ -368,9 +354,9 @@ and close performance gaps before they matter [Assessment-9] [T5]. - **Feedback type:** Immediate and elaborated whenever possible. Automated feedback is acceptable for remember/understand levels. Higher levels need human or structured peer feedback [Assessment-10] [T1]. -- **Purpose:** Close the gap between current and desired performance [Assessment-9] - [T5]. Each checkpoint should give students evidence of where they stand relative - to the rubric criteria. +- **Purpose:** Close the gap between current and desired performance + [Assessment-9] [T5]. Each checkpoint should give students evidence of where they + stand relative to the rubric criteria. **Student-initiated formative assessment:** Where possible, design checkpoints that students can initiate on their own (practice @@ -450,6 +436,7 @@ Consider adding a formative peer review checkpoint to partially address the gap. Before writing the manifest, generate an HTML report so the designer has a single document to read. The report follows the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +{{IDSTACK_RESOLVE}} # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -490,6 +477,7 @@ first, merge in your changes, then pass the full updated section as the payload. The `assessments` payload must include `report_path` set to the value of `$_REPORT_PATH` from the bash block above (e.g., `.idstack/exports//assessment-design.html`). ```bash +{{IDSTACK_RESOLVE}} # Section 1: assessments "$_IDSTACK/bin/idstack-manifest-merge" --section assessments --payload - <<'PAYLOAD' @@ -505,7 +493,12 @@ PAYLOAD If `bin/idstack-manifest-merge` is unavailable: fall back to manual write (Read manifest, modify only the two sections, Write back, preserve all others). -If `.idstack/project.json` does not exist yet, run `bin/idstack-migrate .idstack/project.json` first — that creates a fresh canonical manifest. The merge tool then merges into it. +If `.idstack/project.json` does not exist yet — the standalone case, where the user chose to continue without upstream data — create it first with `--init`, which writes a canonical manifest with every section at its default. The merge tool then merges into it (without a manifest it exits 4): + +```bash +{{IDSTACK_RESOLVE}} +"$_IDSTACK/bin/idstack-migrate" --init .idstack/project.json >/dev/null +``` **Populate the `assessments` section:** @@ -590,7 +583,7 @@ Write the manifest, then confirm to the user: alignment notes. Open it in any browser; the folder is self-contained. - System state: `.idstack/project.json` (the manifest — for downstream skills). -**Next step:** Run `/course-builder` to generate the full course content including +**Next step:** Run `/idstack:course-builder` to generate the full course content including assessment documents, rubric handouts, and assignment instructions." --- @@ -608,6 +601,7 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"assessment-design","event":"completed"}' ``` @@ -618,5 +612,6 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"assessment-design","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/course-builder/SKILL.md b/skills/course-builder/SKILL.md index 6f1d6c3..1aa4f6b 100644 --- a/skills/course-builder/SKILL.md +++ b/skills/course-builder/SKILL.md @@ -50,23 +50,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -78,6 +81,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -147,6 +164,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -180,7 +211,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -188,9 +221,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -234,7 +269,7 @@ Example: "Reminder: this Canvas instance uses custom rubric formatting (discover --- -**Skill-specific manifest check:** If the manifest `course_builder` section already has data, +**Skill-specific manifest check:** If the manifest `course_content` section already has data, ask the user: "I see you've already run this skill. Want to update the results or start fresh?" # Course Builder — Evidence-Based Content Generation @@ -325,32 +360,19 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest - -Before starting content generation, check for an existing project manifest. +## Manifest Inputs (skill-specific) -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. **If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to the - user, offer to fix it, and STOP until it is valid. Never silently overwrite corrupt JSON. - Check which sections are populated. At minimum, you need: - `learning_objectives.ilos` — a non-empty array of classified objectives - `context` — at least `modality` and `timeline` -- If `course_content` section already has data, ask: "I see you've already generated - course content. Want to regenerate from scratch or update specific files?" -- Preserve all existing sections when writing back. **If NO_MANIFEST:** - Say: "I need a project manifest with learning objectives to generate course content. - Run `/needs-analysis` followed by `/learning-objectives` to build the foundation. + Run `/idstack:needs-analysis` followed by `/idstack:learning-objectives` to build the foundation. If you have objectives ready, I can create a minimal manifest to work from — just tell me your learning objectives, course modality, and timeline." - If the user provides objectives directly, create a minimal manifest and proceed. @@ -359,11 +381,11 @@ fi **Nudge for assessment design:** If the manifest exists but has no `assessments` section (or it is empty), say: -"I notice you haven't run `/assessment-design` yet. I can generate basic assessment -documents from the alignment matrix in your objectives, but running `/assessment-design` +"I notice you haven't run `/idstack:assessment-design` yet. I can generate basic assessment +documents from the alignment matrix in your objectives, but running `/idstack:assessment-design` first would give me richer assessment data — rubric criteria, feedback strategies, and assessment type recommendations. Want to continue with what I have, or run -`/assessment-design` first?" +`/idstack:assessment-design` first?" --- @@ -378,7 +400,7 @@ If the manifest exists with upstream data, use it to inform content generation. - **Assessments:** [count] assessments [or 'none — I will derive from alignment matrix'] - **Modality:** [online/hybrid/face-to-face] - **Timeline:** [duration] -- **Expertise reversal flags:** [any flags from /learning-objectives] +- **Expertise reversal flags:** [any flags from /idstack:learning-objectives] Here is what I will generate:" @@ -732,7 +754,7 @@ describe it here.] - Each criterion should map to a specific ILO or component of an ILO - Performance levels should describe observable differences, not just degree words ("excellent analysis" vs. "good analysis" is not useful) -- If the manifest has rubric data from `/assessment-design`, use it directly +- If the manifest has rubric data from `/idstack:assessment-design`, use it directly - If generating rubrics from scratch, ensure the cognitive level of each criterion matches the ILO it measures [Alignment-1] [T5] @@ -777,6 +799,24 @@ continue with remaining files. Before updating the manifest, generate an HTML build report so the designer can see what was generated and why each design choice was made (or, in gap-fill mode, what was skipped and why). The report follows the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -804,6 +844,24 @@ Write the HTML report at the path printed above (`.idstack/exports/ Save the `course_content` section to `.idstack/project.json` via `bin/idstack-manifest-merge`. The merge tool replaces only the named section, preserves every other section verbatim, validates JSON, and atomically updates the top-level `updated` timestamp. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-manifest-merge" --section course_content --payload - <<'PAYLOAD' { "report_path": "/course-builder.html>", @@ -842,7 +900,7 @@ The payload is the **flat section contents** (no enclosing `course_content` key) **Mode field.** Set `mode` to `"build-new"` for a from-scratch generation or `"gap-fill"` when this run was triggered by upstream skills flagging missing artifacts. In `gap-fill` mode, populate `recommended_generation_targets[]` with the per-target outcomes (`status`: `generated|deferred|declined`); in `build-new` mode, leave the array empty. -The `placeholders_used` array lists any fields where placeholder text was used because the user chose to skip or defer those details. This helps downstream skills (like `/course-export`) know what still needs to be filled in. +The `placeholders_used` array lists any fields where placeholder text was used because the user chose to skip or defer those details. This helps downstream skills (like `/idstack:course-export`) know what still needs to be filled in. **Fallback (if `bin/idstack-manifest-merge` is unavailable):** Read the full manifest, modify only the `course_content` section, Write back. Preserve all other sections verbatim. @@ -859,8 +917,8 @@ After writing the manifest, confirm: **Next steps:** - Review and edit the generated files to add your expertise and institutional voice. -- Run `/course-quality-review` to audit the complete course against QM standards and CoI presence. -- Run `/course-export` to package the content as an IMS Common Cartridge or push to Canvas." +- Run `/idstack:course-quality-review` to audit the complete course against QM standards and CoI presence. +- Run `/idstack:course-export` to package the content as an IMS Common Cartridge or push to Canvas." --- @@ -904,7 +962,7 @@ user, run an adversarial self-review if the **Agent tool** is available. - If critical issues remain that require user input, list them in the summary. **If Agent tool is NOT available:** Skip this step. Add a note to the output: -"Tip: Run `/idstack course-quality-review` next for a full alignment audit." +"Tip: Run `/idstack:course-quality-review` next for a full alignment audit." --- @@ -921,7 +979,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -932,10 +990,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1310,6 +1390,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"course-builder","event":"completed"}' ``` @@ -1320,5 +1418,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"course-builder","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/course-builder/SKILL.md.tmpl b/skills/course-builder/SKILL.md.tmpl index 32f387a..8e10c34 100644 --- a/skills/course-builder/SKILL.md.tmpl +++ b/skills/course-builder/SKILL.md.tmpl @@ -18,7 +18,7 @@ allowed-tools: {{PREAMBLE}} -**Skill-specific manifest check:** If the manifest `course_builder` section already has data, +**Skill-specific manifest check:** If the manifest `course_content` section already has data, ask the user: "I see you've already run this skill. Want to update the results or start fresh?" # Course Builder — Evidence-Based Content Generation @@ -109,32 +109,19 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest +## Manifest Inputs (skill-specific) -Before starting content generation, check for an existing project manifest. - -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. **If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to the - user, offer to fix it, and STOP until it is valid. Never silently overwrite corrupt JSON. - Check which sections are populated. At minimum, you need: - `learning_objectives.ilos` — a non-empty array of classified objectives - `context` — at least `modality` and `timeline` -- If `course_content` section already has data, ask: "I see you've already generated - course content. Want to regenerate from scratch or update specific files?" -- Preserve all existing sections when writing back. **If NO_MANIFEST:** - Say: "I need a project manifest with learning objectives to generate course content. - Run `/needs-analysis` followed by `/learning-objectives` to build the foundation. + Run `/idstack:needs-analysis` followed by `/idstack:learning-objectives` to build the foundation. If you have objectives ready, I can create a minimal manifest to work from — just tell me your learning objectives, course modality, and timeline." - If the user provides objectives directly, create a minimal manifest and proceed. @@ -143,11 +130,11 @@ fi **Nudge for assessment design:** If the manifest exists but has no `assessments` section (or it is empty), say: -"I notice you haven't run `/assessment-design` yet. I can generate basic assessment -documents from the alignment matrix in your objectives, but running `/assessment-design` +"I notice you haven't run `/idstack:assessment-design` yet. I can generate basic assessment +documents from the alignment matrix in your objectives, but running `/idstack:assessment-design` first would give me richer assessment data — rubric criteria, feedback strategies, and assessment type recommendations. Want to continue with what I have, or run -`/assessment-design` first?" +`/idstack:assessment-design` first?" --- @@ -162,7 +149,7 @@ If the manifest exists with upstream data, use it to inform content generation. - **Assessments:** [count] assessments [or 'none — I will derive from alignment matrix'] - **Modality:** [online/hybrid/face-to-face] - **Timeline:** [duration] -- **Expertise reversal flags:** [any flags from /learning-objectives] +- **Expertise reversal flags:** [any flags from /idstack:learning-objectives] Here is what I will generate:" @@ -516,7 +503,7 @@ describe it here.] - Each criterion should map to a specific ILO or component of an ILO - Performance levels should describe observable differences, not just degree words ("excellent analysis" vs. "good analysis" is not useful) -- If the manifest has rubric data from `/assessment-design`, use it directly +- If the manifest has rubric data from `/idstack:assessment-design`, use it directly - If generating rubrics from scratch, ensure the cognitive level of each criterion matches the ILO it measures [Alignment-1] [T5] @@ -561,6 +548,7 @@ continue with remaining files. Before updating the manifest, generate an HTML build report so the designer can see what was generated and why each design choice was made (or, in gap-fill mode, what was skipped and why). The report follows the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +{{IDSTACK_RESOLVE}} # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -588,6 +576,7 @@ Write the HTML report at the path printed above (`.idstack/exports/ Save the `course_content` section to `.idstack/project.json` via `bin/idstack-manifest-merge`. The merge tool replaces only the named section, preserves every other section verbatim, validates JSON, and atomically updates the top-level `updated` timestamp. ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-manifest-merge" --section course_content --payload - <<'PAYLOAD' { "report_path": "/course-builder.html>", @@ -626,7 +615,7 @@ The payload is the **flat section contents** (no enclosing `course_content` key) **Mode field.** Set `mode` to `"build-new"` for a from-scratch generation or `"gap-fill"` when this run was triggered by upstream skills flagging missing artifacts. In `gap-fill` mode, populate `recommended_generation_targets[]` with the per-target outcomes (`status`: `generated|deferred|declined`); in `build-new` mode, leave the array empty. -The `placeholders_used` array lists any fields where placeholder text was used because the user chose to skip or defer those details. This helps downstream skills (like `/course-export`) know what still needs to be filled in. +The `placeholders_used` array lists any fields where placeholder text was used because the user chose to skip or defer those details. This helps downstream skills (like `/idstack:course-export`) know what still needs to be filled in. **Fallback (if `bin/idstack-manifest-merge` is unavailable):** Read the full manifest, modify only the `course_content` section, Write back. Preserve all other sections verbatim. @@ -643,8 +632,8 @@ After writing the manifest, confirm: **Next steps:** - Review and edit the generated files to add your expertise and institutional voice. -- Run `/course-quality-review` to audit the complete course against QM standards and CoI presence. -- Run `/course-export` to package the content as an IMS Common Cartridge or push to Canvas." +- Run `/idstack:course-quality-review` to audit the complete course against QM standards and CoI presence. +- Run `/idstack:course-export` to package the content as an IMS Common Cartridge or push to Canvas." --- @@ -688,7 +677,7 @@ user, run an adversarial self-review if the **Agent tool** is available. - If critical issues remain that require user input, list them in the summary. **If Agent tool is NOT available:** Skip this step. Add a note to the output: -"Tip: Run `/idstack course-quality-review` next for a full alignment audit." +"Tip: Run `/idstack:course-quality-review` next for a full alignment audit." --- @@ -705,6 +694,7 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"course-builder","event":"completed"}' ``` @@ -715,5 +705,6 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"course-builder","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/course-export/SKILL.md b/skills/course-export/SKILL.md index 97a3d9c..f45f1e5 100644 --- a/skills/course-export/SKILL.md +++ b/skills/course-export/SKILL.md @@ -50,23 +50,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -78,6 +81,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -147,6 +164,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -180,7 +211,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -188,9 +221,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -234,7 +269,7 @@ Example: "Reminder: this Canvas instance uses custom rubric formatting (discover --- -**Skill-specific manifest check:** If the manifest `course_export` section already has data, +**Skill-specific manifest check:** If the manifest `export_metadata` section already has data, ask the user: "I see you've already run this skill. Want to update the results or start fresh?" ## Pre-Export Readiness Check @@ -242,7 +277,24 @@ ask the user: "I see you've already run this skill. Want to update the results o Before starting the export workflow, run the readiness dashboard: ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-status" --readiness ``` @@ -256,7 +308,7 @@ This is advisory — the user can always choose to export regardless. # Course Export — IMS Common Cartridge & Canvas API You are a course export partner. Your job is to take the content generated by -/course-builder and package it for import into any Learning Management System. +/idstack:course-builder and package it for import into any Learning Management System. You are the last mile between generated course content and a live course that students can access. @@ -276,7 +328,7 @@ manifest that ties them together. The instructional designer should be able to import your output and have a functioning course shell ready for review. You read from two sources: -- The `.idstack/course-content/` directory, where /course-builder writes its +- The `.idstack/course-content/` directory, where /idstack:course-builder writes its generated files (syllabus, module content, assessments, rubrics) - The `.idstack/project.json` manifest, which contains the course structure, learning objectives, and alignment data from upstream skills @@ -294,18 +346,12 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest and Course Content +## Course Content Inputs -Before starting the export, verify that generated course content exists. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` +Before starting the export, verify that generated course content exists: ```bash if [ -d ".idstack/course-content" ]; then @@ -317,20 +363,13 @@ fi ``` **If NO_MANIFEST or NO_CONTENT:** -Tell the user: "I need generated course content to export. Run `/course-builder` +Tell the user: "I need generated course content to export. Run `/idstack:course-builder` first to generate your syllabus, modules, and assessments. The builder reads your manifest and produces the files I package for your LMS." **If MANIFEST_EXISTS but no `course_content` section:** Check whether `.idstack/course-content/` has files. If it does, proceed using -the files directly. If not, nudge for /course-builder. - -**If both exist:** -Read the manifest. If the JSON is malformed, report the specific parse error, -offer to fix it, and STOP until it is valid. Never silently proceed with -corrupt data. - -Preserve all existing manifest sections when writing back. +the files directly. If not, nudge for /idstack:course-builder. --- @@ -342,6 +381,24 @@ produced by the rest of the pipeline. Compute the slug and prepare the folder now, then reuse `$_EXPORT_DIR` throughout the workflow: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Course-slug-based export folder. Required before any artifact write. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -912,7 +969,7 @@ If any items failed: |------|------|-------| | {name} | {type} | {error message} | -You can create these items manually in Canvas, or run `/course-export` again +You can create these items manually in Canvas, or run `/idstack:course-export` again to retry the failed items. ``` @@ -929,7 +986,7 @@ find .idstack/course-content/ -type f | sort ``` If no course content files exist, tell the user: "No course content found in -`.idstack/course-content/`. Run `/course-builder` first to generate content." +`.idstack/course-content/`. Run `/idstack:course-builder` first to generate content." ### C2. Create SCORM package structure @@ -1069,7 +1126,7 @@ Total files: [count] - SCORM API tracking (completion, score reporting) is not included. The LMS will mark the SCO as complete when the learner opens it. - For richer interactivity, author in Articulate Rise or Storyline and - use idstack's /course-quality-review and /red-team on the exported package. + use idstack's /idstack:course-quality-review and /idstack:red-team on the exported package. ``` ### C8. Cleanup @@ -1101,18 +1158,45 @@ After the export completes (any path), write the HTML report at `$_REPORT_PATH` After export completes (any path) and the report is written, update the project manifest with export metadata. +Save results via the section-scoped merge tool — it validates the section name +against the canonical schema, preserves every other section verbatim, bumps the +top-level `updated` timestamp, and writes atomically: + +```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done +# payload file holds ONLY the export_metadata section object (shape below) +"$_IDSTACK/bin/idstack-manifest-merge" --section export_metadata --payload .idstack/.export_metadata-payload.json && rm -f .idstack/.export_metadata-payload.json +``` + +Write the payload object to `.idstack/.export_metadata-payload.json` with the +Write tool first, then run the merge. **If no manifest exists yet** (unlikely +for export, but possible), the merge tool exits with code 4 (`manifest not +found`) — in that case fall back to creating `.idstack/project.json` with the +Write tool, initializing ALL sections from the schema with empty/default values +and placing your `export_metadata` data inside it. + **CRITICAL -- Manifest Integrity Rules:** -1. If a manifest already exists, READ it first with the Read tool. -2. Modify ONLY the `export_metadata` section and the `updated` timestamp. - Preserve all other sections unchanged — `context`, `needs_analysis`, - `learning_objectives`, `quality_review`, `import_metadata`, and any other - sections must remain exactly as they were. -3. Before writing, verify the JSON is valid: matching braces, proper commas, - quoted strings, no trailing commas. -4. Update the top-level `updated` timestamp to reflect the current time. -5. If this is a new manifest (unlikely for export, but possible), initialize - ALL sections with empty/default values so downstream skills find the - expected structure. +1. The payload must be valid JSON: matching braces, proper commas, quoted + strings, no trailing commas. +2. NEVER write secrets (Canvas tokens, credentials) to the manifest or the + payload file. ### Readiness Info @@ -1126,14 +1210,15 @@ Export readiness: Accessibility review: WCAG score 70/100, 1 AA violation ``` -If a section doesn't exist, show: "Not reviewed — run /[skill-name] for analysis." +If a section doesn't exist, show: "Not reviewed — run /idstack: for analysis." This is informational. Export proceeds regardless. The user can choose to address findings first or export now, no AskUserQuestion needed, just show the info and continue. ### Write Export Metadata -Add or update the `export_metadata` field at the root level: +The payload file holds the `export_metadata` section object (shown here wrapped +in its manifest key for context — the payload file contains only the inner object): ```json { @@ -1172,7 +1257,7 @@ sections from the manifest (if they exist). The `verdict` is: - `export_blocked`: not used (export never blocks, advisory only) - Empty string if no reviews exist -Write the manifest, then confirm: +Run the merge, then confirm: "Your export metadata has been saved to `.idstack/project.json`. @@ -1198,7 +1283,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -1209,10 +1294,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1587,6 +1694,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"course-export","event":"completed"}' ``` @@ -1597,5 +1722,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"course-export","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/course-export/SKILL.md.tmpl b/skills/course-export/SKILL.md.tmpl index 69d729a..0d550d6 100644 --- a/skills/course-export/SKILL.md.tmpl +++ b/skills/course-export/SKILL.md.tmpl @@ -18,7 +18,7 @@ allowed-tools: {{PREAMBLE}} -**Skill-specific manifest check:** If the manifest `course_export` section already has data, +**Skill-specific manifest check:** If the manifest `export_metadata` section already has data, ask the user: "I see you've already run this skill. Want to update the results or start fresh?" ## Pre-Export Readiness Check @@ -26,7 +26,7 @@ ask the user: "I see you've already run this skill. Want to update the results o Before starting the export workflow, run the readiness dashboard: ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-status" --readiness ``` @@ -40,7 +40,7 @@ This is advisory — the user can always choose to export regardless. # Course Export — IMS Common Cartridge & Canvas API You are a course export partner. Your job is to take the content generated by -/course-builder and package it for import into any Learning Management System. +/idstack:course-builder and package it for import into any Learning Management System. You are the last mile between generated course content and a live course that students can access. @@ -60,7 +60,7 @@ manifest that ties them together. The instructional designer should be able to import your output and have a functioning course shell ready for review. You read from two sources: -- The `.idstack/course-content/` directory, where /course-builder writes its +- The `.idstack/course-content/` directory, where /idstack:course-builder writes its generated files (syllabus, module content, assessments, rubrics) - The `.idstack/project.json` manifest, which contains the course structure, learning objectives, and alignment data from upstream skills @@ -78,18 +78,12 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest and Course Content +## Course Content Inputs -Before starting the export, verify that generated course content exists. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` +Before starting the export, verify that generated course content exists: ```bash if [ -d ".idstack/course-content" ]; then @@ -101,20 +95,13 @@ fi ``` **If NO_MANIFEST or NO_CONTENT:** -Tell the user: "I need generated course content to export. Run `/course-builder` +Tell the user: "I need generated course content to export. Run `/idstack:course-builder` first to generate your syllabus, modules, and assessments. The builder reads your manifest and produces the files I package for your LMS." **If MANIFEST_EXISTS but no `course_content` section:** Check whether `.idstack/course-content/` has files. If it does, proceed using -the files directly. If not, nudge for /course-builder. - -**If both exist:** -Read the manifest. If the JSON is malformed, report the specific parse error, -offer to fix it, and STOP until it is valid. Never silently proceed with -corrupt data. - -Preserve all existing manifest sections when writing back. +the files directly. If not, nudge for /idstack:course-builder. --- @@ -126,6 +113,7 @@ produced by the rest of the pipeline. Compute the slug and prepare the folder now, then reuse `$_EXPORT_DIR` throughout the workflow: ```bash +{{IDSTACK_RESOLVE}} # Course-slug-based export folder. Required before any artifact write. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -696,7 +684,7 @@ If any items failed: |------|------|-------| | {name} | {type} | {error message} | -You can create these items manually in Canvas, or run `/course-export` again +You can create these items manually in Canvas, or run `/idstack:course-export` again to retry the failed items. ``` @@ -713,7 +701,7 @@ find .idstack/course-content/ -type f | sort ``` If no course content files exist, tell the user: "No course content found in -`.idstack/course-content/`. Run `/course-builder` first to generate content." +`.idstack/course-content/`. Run `/idstack:course-builder` first to generate content." ### C2. Create SCORM package structure @@ -853,7 +841,7 @@ Total files: [count] - SCORM API tracking (completion, score reporting) is not included. The LMS will mark the SCO as complete when the learner opens it. - For richer interactivity, author in Articulate Rise or Storyline and - use idstack's /course-quality-review and /red-team on the exported package. + use idstack's /idstack:course-quality-review and /idstack:red-team on the exported package. ``` ### C8. Cleanup @@ -885,18 +873,28 @@ After the export completes (any path), write the HTML report at `$_REPORT_PATH` After export completes (any path) and the report is written, update the project manifest with export metadata. +Save results via the section-scoped merge tool — it validates the section name +against the canonical schema, preserves every other section verbatim, bumps the +top-level `updated` timestamp, and writes atomically: + +```bash +{{IDSTACK_RESOLVE}} +# payload file holds ONLY the export_metadata section object (shape below) +"$_IDSTACK/bin/idstack-manifest-merge" --section export_metadata --payload .idstack/.export_metadata-payload.json && rm -f .idstack/.export_metadata-payload.json +``` + +Write the payload object to `.idstack/.export_metadata-payload.json` with the +Write tool first, then run the merge. **If no manifest exists yet** (unlikely +for export, but possible), the merge tool exits with code 4 (`manifest not +found`) — in that case fall back to creating `.idstack/project.json` with the +Write tool, initializing ALL sections from the schema with empty/default values +and placing your `export_metadata` data inside it. + **CRITICAL -- Manifest Integrity Rules:** -1. If a manifest already exists, READ it first with the Read tool. -2. Modify ONLY the `export_metadata` section and the `updated` timestamp. - Preserve all other sections unchanged — `context`, `needs_analysis`, - `learning_objectives`, `quality_review`, `import_metadata`, and any other - sections must remain exactly as they were. -3. Before writing, verify the JSON is valid: matching braces, proper commas, - quoted strings, no trailing commas. -4. Update the top-level `updated` timestamp to reflect the current time. -5. If this is a new manifest (unlikely for export, but possible), initialize - ALL sections with empty/default values so downstream skills find the - expected structure. +1. The payload must be valid JSON: matching braces, proper commas, quoted + strings, no trailing commas. +2. NEVER write secrets (Canvas tokens, credentials) to the manifest or the + payload file. ### Readiness Info @@ -910,14 +908,15 @@ Export readiness: Accessibility review: WCAG score 70/100, 1 AA violation ``` -If a section doesn't exist, show: "Not reviewed — run /[skill-name] for analysis." +If a section doesn't exist, show: "Not reviewed — run /idstack: for analysis." This is informational. Export proceeds regardless. The user can choose to address findings first or export now, no AskUserQuestion needed, just show the info and continue. ### Write Export Metadata -Add or update the `export_metadata` field at the root level: +The payload file holds the `export_metadata` section object (shown here wrapped +in its manifest key for context — the payload file contains only the inner object): ```json { @@ -956,7 +955,7 @@ sections from the manifest (if they exist). The `verdict` is: - `export_blocked`: not used (export never blocks, advisory only) - Empty string if no reviews exist -Write the manifest, then confirm: +Run the merge, then confirm: "Your export metadata has been saved to `.idstack/project.json`. @@ -982,6 +981,7 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"course-export","event":"completed"}' ``` @@ -992,5 +992,6 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"course-export","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/course-import/SKILL.md b/skills/course-import/SKILL.md index 96cc71b..b25b799 100644 --- a/skills/course-import/SKILL.md +++ b/skills/course-import/SKILL.md @@ -50,23 +50,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -78,6 +81,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -147,6 +164,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -180,7 +211,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -188,9 +221,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -234,7 +269,7 @@ Example: "Reminder: this Canvas instance uses custom rubric formatting (discover --- -**Skill-specific manifest check:** If the manifest `course_import` section already has data, +**Skill-specific manifest check:** If the manifest `import_metadata` section already has data, ask the user: "I see you've already run this skill. Want to update the results or start fresh?" # Course Import — Universal LMS Course Import @@ -261,7 +296,7 @@ This skill draws primarily from Domain 10 (Online Course Quality) and Domain 2 should detect alignment (or its absence) from the source data. - The revised Bloom's taxonomy (Anderson & Krathwohl) classifies objectives on two dimensions: knowledge type and cognitive process [Alignment-7] [T3]. - Pre-classification during import saves time for /learning-objectives. + Pre-classification during import saves time for /idstack:learning-objectives. ## Evidence Tier Key @@ -274,32 +309,16 @@ Every recommendation and flag includes its evidence tier: --- -## Preamble: Project Manifest - -Before starting the import, check for an existing project manifest. - -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` +## Manifest Inputs (skill-specific) -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error, - offer to fix it, and STOP until it is valid. -- Check which sections have data: - - If `needs_analysis` has data from /needs-analysis: note it. You will PRESERVE - this data. Import adds to task_analysis and learner_profile, does not replace. - - If `learning_objectives` has data from /learning-objectives: ask the user - "You already have learning objectives in your manifest. Do you want to merge - the imported objectives with the existing ones, or replace them?" -- Preserve all sections you don't write to. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. -**If NO_MANIFEST:** -- You will create the manifest at the end of this skill's workflow. +- If `needs_analysis` has data from /idstack:needs-analysis: note it. You will PRESERVE + this data. Import adds to task_analysis and learner_profile, does not replace. +- If `learning_objectives` has data from /idstack:learning-objectives: ask the user + "You already have learning objectives in your manifest. Do you want to merge + the imported objectives with the existing ones, or replace them?" --- @@ -632,7 +651,7 @@ it for up to 10 pages (500 items max per endpoint). After 500 items, stop and no **Error handling for each call:** - 404: Skip this endpoint, note what's missing - 429: Wait 10 seconds, retry once. If still 429: "Canvas is rate-limiting. - Wait a minute and try `/course-import` again." + Wait a minute and try `/idstack:course-import` again." - Timeout: "Canvas didn't respond for [endpoint]. Continuing with what we have." ### C4. Map API response to course structure @@ -795,7 +814,7 @@ Continue to Step 2 (Quality Flags). ## Step 2: Quick-Scan Quality Flags After extracting course structure from ANY input method, scan for obvious quality -issues. This is NOT a full /course-quality-review. This is a quick triage that +issues. This is NOT a full /idstack:course-quality-review. This is a quick triage that flags problems visible in the structural data alone. **Structural flags:** @@ -832,11 +851,11 @@ Found {N} flags during import: {list each flag with ⚠ prefix} These are quick observations from the course structure, not a full review. -Run /course-quality-review for an evidence-based audit with specific recommendations. +Run /idstack:course-quality-review for an evidence-based audit with specific recommendations. ``` If zero flags: "No obvious structural issues detected during import. Run -/course-quality-review for a deeper analysis." +/idstack:course-quality-review for a deeper analysis." --- @@ -914,10 +933,10 @@ For each objective: 4. **Confidence level:** - High: verb maps clearly to one Bloom's level - Ambiguous: verb could map to multiple levels (e.g., "analyze", "demonstrate") - Mark as "verify with /learning-objectives" [Alignment-12] [T2] + Mark as "verify with /idstack:learning-objectives" [Alignment-12] [T2] 5. **Set alignment_status to "imported-unverified"** — the user should run - /learning-objectives to verify and check bidirectional alignment + /idstack:learning-objectives to verify and check bidirectional alignment Assign ILO IDs: ILO-1, ILO-2, etc. @@ -932,7 +951,7 @@ Present for review: | ILO-2 | [text] | [dim] | [proc] | ambiguous — verify | ... -All classifications are marked "imported-unverified." Run /learning-objectives +All classifications are marked "imported-unverified." Run /idstack:learning-objectives to verify Bloom's levels and check alignment with activities and assessments. ``` @@ -943,6 +962,24 @@ to verify Bloom's levels and check alignment with activities and assessments. Before writing the manifest, generate an HTML report so the designer has a single document about what came in and where the quality flags are. The report follows the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -980,7 +1017,7 @@ Create or update the project manifest. **CRITICAL — Manifest Integrity Rules:** 1. If a manifest already exists, READ it first. 2. PRESERVE sections populated by other skills (especially needs_analysis from - /needs-analysis). Import ADDS to these sections, does not replace. + /idstack:needs-analysis). Import ADDS to these sections, does not replace. 3. If learning_objectives already has data and the user chose "merge," combine imported ILOs with existing ones (use new ILO IDs that don't conflict). 4. Include the COMPLETE schema structure. Do not omit fields. @@ -988,7 +1025,7 @@ Create or update the project manifest. 6. Update the `updated` timestamp. 7. **NEVER write the Canvas API token to the manifest or any file.** -**Fields populated by /course-import:** +**Fields populated by /idstack:course-import:** - `project_name` — from course title - `context.modality` — inferred from course structure (async=online, sync sessions=hybrid) @@ -1043,11 +1080,11 @@ Write the manifest, then confirm: - System state: `.idstack/project.json` (the manifest — for downstream skills). **Recommended next steps:** -1. `/course-quality-review` — Full evidence-based audit with QM standards and +1. `/idstack:course-quality-review` — Full evidence-based audit with QM standards and CoI presence analysis -2. `/learning-objectives` — Verify Bloom's classifications and check +2. `/idstack:learning-objectives` — Verify Bloom's classifications and check bidirectional alignment (objectives ↔ activities ↔ assessments) -3. `/needs-analysis` — Add organizational context and learner profile data +3. `/idstack:needs-analysis` — Add organizational context and learner profile data that can't be extracted from the course structure alone ``` @@ -1066,7 +1103,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -1077,10 +1114,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1455,6 +1514,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"course-import","event":"completed"}' ``` @@ -1465,5 +1542,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"course-import","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/course-import/SKILL.md.tmpl b/skills/course-import/SKILL.md.tmpl index 55f3954..3faa4c6 100644 --- a/skills/course-import/SKILL.md.tmpl +++ b/skills/course-import/SKILL.md.tmpl @@ -18,7 +18,7 @@ allowed-tools: {{PREAMBLE}} -**Skill-specific manifest check:** If the manifest `course_import` section already has data, +**Skill-specific manifest check:** If the manifest `import_metadata` section already has data, ask the user: "I see you've already run this skill. Want to update the results or start fresh?" # Course Import — Universal LMS Course Import @@ -45,7 +45,7 @@ This skill draws primarily from Domain 10 (Online Course Quality) and Domain 2 should detect alignment (or its absence) from the source data. - The revised Bloom's taxonomy (Anderson & Krathwohl) classifies objectives on two dimensions: knowledge type and cognitive process [Alignment-7] [T3]. - Pre-classification during import saves time for /learning-objectives. + Pre-classification during import saves time for /idstack:learning-objectives. ## Evidence Tier Key @@ -58,32 +58,16 @@ Every recommendation and flag includes its evidence tier: --- -## Preamble: Project Manifest +## Manifest Inputs (skill-specific) -Before starting the import, check for an existing project manifest. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` - -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error, - offer to fix it, and STOP until it is valid. -- Check which sections have data: - - If `needs_analysis` has data from /needs-analysis: note it. You will PRESERVE - this data. Import adds to task_analysis and learner_profile, does not replace. - - If `learning_objectives` has data from /learning-objectives: ask the user - "You already have learning objectives in your manifest. Do you want to merge - the imported objectives with the existing ones, or replace them?" -- Preserve all sections you don't write to. - -**If NO_MANIFEST:** -- You will create the manifest at the end of this skill's workflow. +- If `needs_analysis` has data from /idstack:needs-analysis: note it. You will PRESERVE + this data. Import adds to task_analysis and learner_profile, does not replace. +- If `learning_objectives` has data from /idstack:learning-objectives: ask the user + "You already have learning objectives in your manifest. Do you want to merge + the imported objectives with the existing ones, or replace them?" --- @@ -416,7 +400,7 @@ it for up to 10 pages (500 items max per endpoint). After 500 items, stop and no **Error handling for each call:** - 404: Skip this endpoint, note what's missing - 429: Wait 10 seconds, retry once. If still 429: "Canvas is rate-limiting. - Wait a minute and try `/course-import` again." + Wait a minute and try `/idstack:course-import` again." - Timeout: "Canvas didn't respond for [endpoint]. Continuing with what we have." ### C4. Map API response to course structure @@ -579,7 +563,7 @@ Continue to Step 2 (Quality Flags). ## Step 2: Quick-Scan Quality Flags After extracting course structure from ANY input method, scan for obvious quality -issues. This is NOT a full /course-quality-review. This is a quick triage that +issues. This is NOT a full /idstack:course-quality-review. This is a quick triage that flags problems visible in the structural data alone. **Structural flags:** @@ -616,11 +600,11 @@ Found {N} flags during import: {list each flag with ⚠ prefix} These are quick observations from the course structure, not a full review. -Run /course-quality-review for an evidence-based audit with specific recommendations. +Run /idstack:course-quality-review for an evidence-based audit with specific recommendations. ``` If zero flags: "No obvious structural issues detected during import. Run -/course-quality-review for a deeper analysis." +/idstack:course-quality-review for a deeper analysis." --- @@ -698,10 +682,10 @@ For each objective: 4. **Confidence level:** - High: verb maps clearly to one Bloom's level - Ambiguous: verb could map to multiple levels (e.g., "analyze", "demonstrate") - Mark as "verify with /learning-objectives" [Alignment-12] [T2] + Mark as "verify with /idstack:learning-objectives" [Alignment-12] [T2] 5. **Set alignment_status to "imported-unverified"** — the user should run - /learning-objectives to verify and check bidirectional alignment + /idstack:learning-objectives to verify and check bidirectional alignment Assign ILO IDs: ILO-1, ILO-2, etc. @@ -716,7 +700,7 @@ Present for review: | ILO-2 | [text] | [dim] | [proc] | ambiguous — verify | ... -All classifications are marked "imported-unverified." Run /learning-objectives +All classifications are marked "imported-unverified." Run /idstack:learning-objectives to verify Bloom's levels and check alignment with activities and assessments. ``` @@ -727,6 +711,7 @@ to verify Bloom's levels and check alignment with activities and assessments. Before writing the manifest, generate an HTML report so the designer has a single document about what came in and where the quality flags are. The report follows the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +{{IDSTACK_RESOLVE}} # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -764,7 +749,7 @@ Create or update the project manifest. **CRITICAL — Manifest Integrity Rules:** 1. If a manifest already exists, READ it first. 2. PRESERVE sections populated by other skills (especially needs_analysis from - /needs-analysis). Import ADDS to these sections, does not replace. + /idstack:needs-analysis). Import ADDS to these sections, does not replace. 3. If learning_objectives already has data and the user chose "merge," combine imported ILOs with existing ones (use new ILO IDs that don't conflict). 4. Include the COMPLETE schema structure. Do not omit fields. @@ -772,7 +757,7 @@ Create or update the project manifest. 6. Update the `updated` timestamp. 7. **NEVER write the Canvas API token to the manifest or any file.** -**Fields populated by /course-import:** +**Fields populated by /idstack:course-import:** - `project_name` — from course title - `context.modality` — inferred from course structure (async=online, sync sessions=hybrid) @@ -827,11 +812,11 @@ Write the manifest, then confirm: - System state: `.idstack/project.json` (the manifest — for downstream skills). **Recommended next steps:** -1. `/course-quality-review` — Full evidence-based audit with QM standards and +1. `/idstack:course-quality-review` — Full evidence-based audit with QM standards and CoI presence analysis -2. `/learning-objectives` — Verify Bloom's classifications and check +2. `/idstack:learning-objectives` — Verify Bloom's classifications and check bidirectional alignment (objectives ↔ activities ↔ assessments) -3. `/needs-analysis` — Add organizational context and learner profile data +3. `/idstack:needs-analysis` — Add organizational context and learner profile data that can't be extracted from the course structure alone ``` @@ -850,6 +835,7 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"course-import","event":"completed"}' ``` @@ -860,5 +846,6 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"course-import","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/course-quality-review/SKILL.md b/skills/course-quality-review/SKILL.md index b50c6d2..b35db8f 100644 --- a/skills/course-quality-review/SKILL.md +++ b/skills/course-quality-review/SKILL.md @@ -50,23 +50,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -78,6 +81,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -147,6 +164,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -180,7 +211,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -188,9 +221,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -234,7 +269,7 @@ Example: "Reminder: this Canvas instance uses custom rubric formatting (discover --- -**Skill-specific manifest check:** If the manifest `course_quality_review` section already has data, +**Skill-specific manifest check:** If the manifest `quality_review` section already has data, ask the user: "I see you've already run this skill. Want to update the results or start fresh?" # Course Quality Review — QM-Aligned Audit with CoI Presence Layer @@ -300,32 +335,14 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest +## Manifest Inputs (skill-specific) -Before starting the review, check for an existing project manifest. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` - -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to - the user, offer to fix it, and STOP until it is valid. Never silently overwrite - corrupt JSON. - Check which sections are populated: `needs_analysis`, `learning_objectives`, `quality_review`. This determines your review mode. -- If `quality_review` section already has data, ask: "I see a previous quality - review. Want to update it or start fresh?" -- Preserve all existing sections when writing back. - -**If NO_MANIFEST:** -- That is fine. This skill works standalone. You will create the manifest at the - end if the user wants to save results. +- Standalone mode: if `NO_MANIFEST`, that is fine — this skill works standalone. --- @@ -662,24 +679,24 @@ Scan the course design for these cognitive load violations: - **Split attention:** Content explanation is separated from the diagram, example, or visual it references. Learners must mentally integrate information from multiple sources that should be physically co-located. Flag as warning. - Fix: run /course-builder to regenerate module with integrated content. + Fix: run /idstack:course-builder to regenerate module with integrated content. - **Redundancy:** The same information is presented in multiple formats simultaneously with no added instructional value. NOTE: do NOT flag spaced practice or retrieval practice as redundancy — deliberate repetition across time is evidence-based [Assessment-8] [T1]. Only flag identical information presented simultaneously (e.g., reading aloud on-screen text verbatim). Flag as info. - Fix: run /course-builder to consolidate redundant presentations. + Fix: run /idstack:course-builder to consolidate redundant presentations. - **Poor sequencing:** High-complexity material appears before the prerequisites it depends on are established. Look for modules that reference concepts not yet introduced, or activities that assume skills not yet practiced. Flag as critical. - Fix: run /course-builder to resequence modules based on prerequisite chain. + Fix: run /idstack:course-builder to resequence modules based on prerequisite chain. - **Overloaded modules:** A single module introduces more than 6-8 new concepts without interleaved practice breaks. Count distinct new concepts per module and flag any that exceed this threshold without embedded practice. Flag as warning. - Fix: run /course-builder to split module or add practice checkpoints. + Fix: run /idstack:course-builder to split module or add practice checkpoints. ### Check 2: Multimedia Principle Violations @@ -690,22 +707,22 @@ Scan for violations of Mayer's multimedia learning principles: - **Spatial contiguity:** Text and related visuals are physically separated (e.g., figure on one page, explanation on another; caption far from image). Flag as warning. - Fix: run /course-builder to co-locate text and visuals. + Fix: run /idstack:course-builder to co-locate text and visuals. - **Temporal contiguity:** Narration and visuals are not synchronized (e.g., a video describes a diagram that appears 30 seconds later). Flag as warning. - Fix: run /course-builder to synchronize narration with visual presentation. + Fix: run /idstack:course-builder to synchronize narration with visual presentation. - **Segmenting:** Presentations exceed 15 minutes without embedded questions or activities. Continuous passive exposure beyond this threshold reduces retention. Flag as warning. - Fix: run /course-builder to segment long presentations with embedded activities. + Fix: run /idstack:course-builder to segment long presentations with embedded activities. - **Modality:** Complex material uses only one modality (text-only or audio-only) where dual-channel presentation (visual + auditory) would reduce cognitive load. Flag as info. - Fix: run /course-builder to add complementary modality. + Fix: run /idstack:course-builder to add complementary modality. - **Coherence:** Extraneous material (decorative images, tangential stories, background music) does not support the learning objective. Seductive details hurt learning. Flag as info. - Fix: run /course-builder to remove extraneous elements. + Fix: run /idstack:course-builder to remove extraneous elements. ### Check 3: Feedback Quality @@ -718,19 +735,19 @@ Scan the assessment design for feedback quality issues: creation that only report correct/incorrect provide no learning mechanism. Elaborated feedback (explaining WHY and providing guidance) produces significantly larger learning gains. Flag as critical. - Fix: run /assessment-design to add elaborated feedback for higher-order assessments. + Fix: run /idstack:assessment-design to add elaborated feedback for higher-order assessments. - **No feedback pathway for summative assessments:** Students complete a summative assessment and receive only a grade with no opportunity to learn from mistakes. Flag as warning. - Fix: run /assessment-design to add post-submission feedback or reflection activity. + Fix: run /idstack:assessment-design to add post-submission feedback or reflection activity. - **Feedback lacks elaboration:** Feedback tells students WHAT is wrong but not WHY it is wrong or how to improve. Flag as warning. - Fix: run /assessment-design to add elaborated feedback with explanations. + Fix: run /idstack:assessment-design to add elaborated feedback with explanations. - **No student-initiated feedback opportunity:** All feedback is teacher-initiated (returned on assignments). There is no mechanism for students to seek feedback when they need it (e.g., self-check quizzes, rubric previews, peer review). Flag as info. - Fix: run /assessment-design to add formative self-check opportunities. + Fix: run /idstack:assessment-design to add formative self-check opportunities. ### Check 4: Expertise Reversal @@ -739,29 +756,29 @@ Scan the assessment design for feedback quality issues: If a learner profile is available (from manifest `needs_analysis.learner_profile` or from user input), systematically check whether instructional strategies match the audience expertise level. If no learner profile exists, flag the absence as -a warning and recommend running /needs-analysis. +a warning and recommend running /idstack:needs-analysis. - **Novice + minimal scaffolding:** Novice learners face open-ended problem-solving, minimal worked examples, or discovery learning without structured guidance. This causes cognitive overload and poor learning outcomes. Flag as critical. - Fix: run /course-builder to regenerate module with scaffolding and worked examples. + Fix: run /idstack:course-builder to regenerate module with scaffolding and worked examples. - **Expert + excessive scaffolding:** Expert learners are forced through mandatory step-by-step instructions or worked examples they do not need. Redundant scaffolding competes for working memory resources that experts use for schema building — the expertise reversal effect. Flag as warning. - Fix: run /course-builder to offer advanced-track options that skip scaffolding. + Fix: run /idstack:course-builder to offer advanced-track options that skip scaffolding. - **Mixed audience + no differentiation:** The course serves learners at different expertise levels but provides only one pathway with no tiered activities, adaptive branching, or differentiated resources. Flag as warning. - Fix: run /needs-analysis to establish a detailed learner profile, then - run /course-builder to create differentiated pathways. + Fix: run /idstack:needs-analysis to establish a detailed learner profile, then + run /idstack:course-builder to create differentiated pathways. - **Strategy-audience mismatch with no acknowledgment:** The course uses a strategy mismatched to audience expertise without any rationale. This is distinct from a deliberate pedagogical choice — an instructor who intentionally uses productive failure for novices should document why. Undocumented mismatches are flags. Flag as info. - Fix: run /course-builder to add instructor rationale or adjust strategy. + Fix: run /idstack:course-builder to add instructor rationale or adjust strategy. --- @@ -784,9 +801,9 @@ Present the **Top 3 fixes for maximum impact**: ### Top 3 Quick Wins | # | Finding | Impact | Skill to Run | |---|---------|--------|--------------| -| 1 | [finding] | [score] (T?/sev/ease) | /skill-name | -| 2 | [finding] | [score] (T?/sev/ease) | /skill-name | -| 3 | [finding] | [score] (T?/sev/ease) | /skill-name | +| 1 | [finding] | [score] (T?/sev/ease) | /idstack: | +| 2 | [finding] | [score] (T?/sev/ease) | /idstack: | +| 3 | [finding] | [score] (T?/sev/ease) | /idstack: | ``` Estimate the ease of fix based on: S = a single skill run fixes it, M = requires @@ -853,9 +870,9 @@ Then present the detailed findings: ### Top 3 Quick Wins | # | Finding | Impact | Skill to Run | |---|---------|--------|--------------| -| 1 | [finding] | [score] (T?/sev/ease) | /skill-name | -| 2 | [finding] | [score] (T?/sev/ease) | /skill-name | -| 3 | [finding] | [score] (T?/sev/ease) | /skill-name | +| 1 | [finding] | [score] (T?/sev/ease) | /idstack: | +| 2 | [finding] | [score] (T?/sev/ease) | /idstack: | +| 3 | [finding] | [score] (T?/sev/ease) | /idstack: | ``` ### Scoring Rubric @@ -877,38 +894,64 @@ Calculate the overall score from these components (total: 100 points): When recommending fixes, point users to the appropriate idstack skill: -- Misaligned or weak ILOs: "Run `/learning-objectives` to realign ILO-3 with +- Misaligned or weak ILOs: "Run `/idstack:learning-objectives` to realign ILO-3 with its assessment." -- Missing learner profile: "Run `/needs-analysis` to establish the learner +- Missing learner profile: "Run `/idstack:needs-analysis` to establish the learner profile that is currently missing." -- No task analysis: "Run `/needs-analysis` — the task analysis will inform +- No task analysis: "Run `/idstack:needs-analysis` — the task analysis will inform which activities are core vs. reference." -- Weak alignment chain: "Run `/learning-objectives` to rebuild the alignment +- Weak alignment chain: "Run `/idstack:learning-objectives` to rebuild the alignment matrix from your task analysis." --- ## Write Manifest -After completing the review, save results to the project manifest. +After completing the review, save results via the section-scoped merge tool — it +validates the section name against the canonical schema, preserves every other +section verbatim, bumps the top-level `updated` timestamp, and writes atomically: + +```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done +# payload file holds ONLY the quality_review section object (shape below) +"$_IDSTACK/bin/idstack-manifest-merge" --section quality_review --payload .idstack/.quality_review-payload.json && rm -f .idstack/.quality_review-payload.json +``` + +Write the payload object to `.idstack/.quality_review-payload.json` with the +Write tool first, then run the merge. **If no manifest exists yet**, the merge +tool exits with an error (exit code 4, `manifest not found`) — in that case fall +back to creating `.idstack/project.json` with the Write tool, initializing ALL +sections from the schema (including `context`, `needs_analysis`, and +`learning_objectives`) with empty/default values and placing your +`quality_review` data inside it. **CRITICAL — Manifest Integrity Rules:** -1. If a manifest already exists, READ it first with the Read tool. -2. Modify ONLY the `quality_review` section. Preserve all other sections - unchanged — `context`, `needs_analysis`, `learning_objectives`, and any - other sections must remain exactly as they were. -3. Before writing, verify the JSON is valid: matching braces, proper commas, - quoted strings, no trailing commas. -4. Update the top-level `updated` timestamp to reflect the current time. -5. If this is a new manifest, initialize ALL sections (including `context`, - `needs_analysis`, and `learning_objectives`) with empty/default values so - downstream skills find the expected structure. - -Populate the `quality_review` section with: +1. The payload must be valid JSON: matching braces, proper commas, quoted + strings, no trailing commas. The merge tool rejects malformed payloads + (exit code 1). +2. Never write secrets or credentials into the manifest. + +Payload shape — the file contains ONLY the section object, no `"quality_review"` wrapper: ```json { - "quality_review": { "report_path": "/course-quality-review.html>", "last_reviewed": "ISO-8601 timestamp", "qm_standards": { @@ -947,7 +990,7 @@ Populate the `quality_review` section with: "evidence_tier": "T1-T5", "severity": "critical|warning|info", "ease": "S|M|L", - "fix_skill": "/skill-name" + "fix_skill": "/idstack:" } ], "recommendations": [ @@ -958,13 +1001,12 @@ Populate the `quality_review` section with: "fix": "..." } ] - } } ``` -When writing the manifest: +When building the payload: - Populate ALL fields in the `quality_review` section from the analysis above. -- Update the top-level `updated` timestamp to reflect the current time. +- The merge tool updates the top-level `updated` timestamp automatically. ### Score Trending Display @@ -984,6 +1026,24 @@ One source of truth per data point. After writing the manifest, generate a shareable HTML report. The report follows the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -1042,7 +1102,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -1053,10 +1113,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1433,6 +1515,24 @@ Include the overall_score so the preamble's context recovery can display score t across sessions (e.g., "Quality score trend: 62 -> 72 -> 78 over 3 reviews"). ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"course-quality-review","event":"completed","score":OVERALL_SCORE,"dimensions":{"teaching_presence":TP,"social_presence":SP,"cognitive_presence":CP}}' ``` @@ -1443,5 +1543,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"course-quality-review","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/course-quality-review/SKILL.md.tmpl b/skills/course-quality-review/SKILL.md.tmpl index 51d8b67..f2fd62a 100644 --- a/skills/course-quality-review/SKILL.md.tmpl +++ b/skills/course-quality-review/SKILL.md.tmpl @@ -18,7 +18,7 @@ allowed-tools: {{PREAMBLE}} -**Skill-specific manifest check:** If the manifest `course_quality_review` section already has data, +**Skill-specific manifest check:** If the manifest `quality_review` section already has data, ask the user: "I see you've already run this skill. Want to update the results or start fresh?" # Course Quality Review — QM-Aligned Audit with CoI Presence Layer @@ -84,32 +84,14 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest +## Manifest Inputs (skill-specific) -Before starting the review, check for an existing project manifest. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` - -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to - the user, offer to fix it, and STOP until it is valid. Never silently overwrite - corrupt JSON. - Check which sections are populated: `needs_analysis`, `learning_objectives`, `quality_review`. This determines your review mode. -- If `quality_review` section already has data, ask: "I see a previous quality - review. Want to update it or start fresh?" -- Preserve all existing sections when writing back. - -**If NO_MANIFEST:** -- That is fine. This skill works standalone. You will create the manifest at the - end if the user wants to save results. +- Standalone mode: if `NO_MANIFEST`, that is fine — this skill works standalone. --- @@ -446,24 +428,24 @@ Scan the course design for these cognitive load violations: - **Split attention:** Content explanation is separated from the diagram, example, or visual it references. Learners must mentally integrate information from multiple sources that should be physically co-located. Flag as warning. - Fix: run /course-builder to regenerate module with integrated content. + Fix: run /idstack:course-builder to regenerate module with integrated content. - **Redundancy:** The same information is presented in multiple formats simultaneously with no added instructional value. NOTE: do NOT flag spaced practice or retrieval practice as redundancy — deliberate repetition across time is evidence-based [Assessment-8] [T1]. Only flag identical information presented simultaneously (e.g., reading aloud on-screen text verbatim). Flag as info. - Fix: run /course-builder to consolidate redundant presentations. + Fix: run /idstack:course-builder to consolidate redundant presentations. - **Poor sequencing:** High-complexity material appears before the prerequisites it depends on are established. Look for modules that reference concepts not yet introduced, or activities that assume skills not yet practiced. Flag as critical. - Fix: run /course-builder to resequence modules based on prerequisite chain. + Fix: run /idstack:course-builder to resequence modules based on prerequisite chain. - **Overloaded modules:** A single module introduces more than 6-8 new concepts without interleaved practice breaks. Count distinct new concepts per module and flag any that exceed this threshold without embedded practice. Flag as warning. - Fix: run /course-builder to split module or add practice checkpoints. + Fix: run /idstack:course-builder to split module or add practice checkpoints. ### Check 2: Multimedia Principle Violations @@ -474,22 +456,22 @@ Scan for violations of Mayer's multimedia learning principles: - **Spatial contiguity:** Text and related visuals are physically separated (e.g., figure on one page, explanation on another; caption far from image). Flag as warning. - Fix: run /course-builder to co-locate text and visuals. + Fix: run /idstack:course-builder to co-locate text and visuals. - **Temporal contiguity:** Narration and visuals are not synchronized (e.g., a video describes a diagram that appears 30 seconds later). Flag as warning. - Fix: run /course-builder to synchronize narration with visual presentation. + Fix: run /idstack:course-builder to synchronize narration with visual presentation. - **Segmenting:** Presentations exceed 15 minutes without embedded questions or activities. Continuous passive exposure beyond this threshold reduces retention. Flag as warning. - Fix: run /course-builder to segment long presentations with embedded activities. + Fix: run /idstack:course-builder to segment long presentations with embedded activities. - **Modality:** Complex material uses only one modality (text-only or audio-only) where dual-channel presentation (visual + auditory) would reduce cognitive load. Flag as info. - Fix: run /course-builder to add complementary modality. + Fix: run /idstack:course-builder to add complementary modality. - **Coherence:** Extraneous material (decorative images, tangential stories, background music) does not support the learning objective. Seductive details hurt learning. Flag as info. - Fix: run /course-builder to remove extraneous elements. + Fix: run /idstack:course-builder to remove extraneous elements. ### Check 3: Feedback Quality @@ -502,19 +484,19 @@ Scan the assessment design for feedback quality issues: creation that only report correct/incorrect provide no learning mechanism. Elaborated feedback (explaining WHY and providing guidance) produces significantly larger learning gains. Flag as critical. - Fix: run /assessment-design to add elaborated feedback for higher-order assessments. + Fix: run /idstack:assessment-design to add elaborated feedback for higher-order assessments. - **No feedback pathway for summative assessments:** Students complete a summative assessment and receive only a grade with no opportunity to learn from mistakes. Flag as warning. - Fix: run /assessment-design to add post-submission feedback or reflection activity. + Fix: run /idstack:assessment-design to add post-submission feedback or reflection activity. - **Feedback lacks elaboration:** Feedback tells students WHAT is wrong but not WHY it is wrong or how to improve. Flag as warning. - Fix: run /assessment-design to add elaborated feedback with explanations. + Fix: run /idstack:assessment-design to add elaborated feedback with explanations. - **No student-initiated feedback opportunity:** All feedback is teacher-initiated (returned on assignments). There is no mechanism for students to seek feedback when they need it (e.g., self-check quizzes, rubric previews, peer review). Flag as info. - Fix: run /assessment-design to add formative self-check opportunities. + Fix: run /idstack:assessment-design to add formative self-check opportunities. ### Check 4: Expertise Reversal @@ -523,29 +505,29 @@ Scan the assessment design for feedback quality issues: If a learner profile is available (from manifest `needs_analysis.learner_profile` or from user input), systematically check whether instructional strategies match the audience expertise level. If no learner profile exists, flag the absence as -a warning and recommend running /needs-analysis. +a warning and recommend running /idstack:needs-analysis. - **Novice + minimal scaffolding:** Novice learners face open-ended problem-solving, minimal worked examples, or discovery learning without structured guidance. This causes cognitive overload and poor learning outcomes. Flag as critical. - Fix: run /course-builder to regenerate module with scaffolding and worked examples. + Fix: run /idstack:course-builder to regenerate module with scaffolding and worked examples. - **Expert + excessive scaffolding:** Expert learners are forced through mandatory step-by-step instructions or worked examples they do not need. Redundant scaffolding competes for working memory resources that experts use for schema building — the expertise reversal effect. Flag as warning. - Fix: run /course-builder to offer advanced-track options that skip scaffolding. + Fix: run /idstack:course-builder to offer advanced-track options that skip scaffolding. - **Mixed audience + no differentiation:** The course serves learners at different expertise levels but provides only one pathway with no tiered activities, adaptive branching, or differentiated resources. Flag as warning. - Fix: run /needs-analysis to establish a detailed learner profile, then - run /course-builder to create differentiated pathways. + Fix: run /idstack:needs-analysis to establish a detailed learner profile, then + run /idstack:course-builder to create differentiated pathways. - **Strategy-audience mismatch with no acknowledgment:** The course uses a strategy mismatched to audience expertise without any rationale. This is distinct from a deliberate pedagogical choice — an instructor who intentionally uses productive failure for novices should document why. Undocumented mismatches are flags. Flag as info. - Fix: run /course-builder to add instructor rationale or adjust strategy. + Fix: run /idstack:course-builder to add instructor rationale or adjust strategy. --- @@ -568,9 +550,9 @@ Present the **Top 3 fixes for maximum impact**: ### Top 3 Quick Wins | # | Finding | Impact | Skill to Run | |---|---------|--------|--------------| -| 1 | [finding] | [score] (T?/sev/ease) | /skill-name | -| 2 | [finding] | [score] (T?/sev/ease) | /skill-name | -| 3 | [finding] | [score] (T?/sev/ease) | /skill-name | +| 1 | [finding] | [score] (T?/sev/ease) | /idstack: | +| 2 | [finding] | [score] (T?/sev/ease) | /idstack: | +| 3 | [finding] | [score] (T?/sev/ease) | /idstack: | ``` Estimate the ease of fix based on: S = a single skill run fixes it, M = requires @@ -637,9 +619,9 @@ Then present the detailed findings: ### Top 3 Quick Wins | # | Finding | Impact | Skill to Run | |---|---------|--------|--------------| -| 1 | [finding] | [score] (T?/sev/ease) | /skill-name | -| 2 | [finding] | [score] (T?/sev/ease) | /skill-name | -| 3 | [finding] | [score] (T?/sev/ease) | /skill-name | +| 1 | [finding] | [score] (T?/sev/ease) | /idstack: | +| 2 | [finding] | [score] (T?/sev/ease) | /idstack: | +| 3 | [finding] | [score] (T?/sev/ease) | /idstack: | ``` ### Scoring Rubric @@ -661,38 +643,47 @@ Calculate the overall score from these components (total: 100 points): When recommending fixes, point users to the appropriate idstack skill: -- Misaligned or weak ILOs: "Run `/learning-objectives` to realign ILO-3 with +- Misaligned or weak ILOs: "Run `/idstack:learning-objectives` to realign ILO-3 with its assessment." -- Missing learner profile: "Run `/needs-analysis` to establish the learner +- Missing learner profile: "Run `/idstack:needs-analysis` to establish the learner profile that is currently missing." -- No task analysis: "Run `/needs-analysis` — the task analysis will inform +- No task analysis: "Run `/idstack:needs-analysis` — the task analysis will inform which activities are core vs. reference." -- Weak alignment chain: "Run `/learning-objectives` to rebuild the alignment +- Weak alignment chain: "Run `/idstack:learning-objectives` to rebuild the alignment matrix from your task analysis." --- ## Write Manifest -After completing the review, save results to the project manifest. +After completing the review, save results via the section-scoped merge tool — it +validates the section name against the canonical schema, preserves every other +section verbatim, bumps the top-level `updated` timestamp, and writes atomically: + +```bash +{{IDSTACK_RESOLVE}} +# payload file holds ONLY the quality_review section object (shape below) +"$_IDSTACK/bin/idstack-manifest-merge" --section quality_review --payload .idstack/.quality_review-payload.json && rm -f .idstack/.quality_review-payload.json +``` + +Write the payload object to `.idstack/.quality_review-payload.json` with the +Write tool first, then run the merge. **If no manifest exists yet**, the merge +tool exits with an error (exit code 4, `manifest not found`) — in that case fall +back to creating `.idstack/project.json` with the Write tool, initializing ALL +sections from the schema (including `context`, `needs_analysis`, and +`learning_objectives`) with empty/default values and placing your +`quality_review` data inside it. **CRITICAL — Manifest Integrity Rules:** -1. If a manifest already exists, READ it first with the Read tool. -2. Modify ONLY the `quality_review` section. Preserve all other sections - unchanged — `context`, `needs_analysis`, `learning_objectives`, and any - other sections must remain exactly as they were. -3. Before writing, verify the JSON is valid: matching braces, proper commas, - quoted strings, no trailing commas. -4. Update the top-level `updated` timestamp to reflect the current time. -5. If this is a new manifest, initialize ALL sections (including `context`, - `needs_analysis`, and `learning_objectives`) with empty/default values so - downstream skills find the expected structure. - -Populate the `quality_review` section with: +1. The payload must be valid JSON: matching braces, proper commas, quoted + strings, no trailing commas. The merge tool rejects malformed payloads + (exit code 1). +2. Never write secrets or credentials into the manifest. + +Payload shape — the file contains ONLY the section object, no `"quality_review"` wrapper: ```json { - "quality_review": { "report_path": "/course-quality-review.html>", "last_reviewed": "ISO-8601 timestamp", "qm_standards": { @@ -731,7 +722,7 @@ Populate the `quality_review` section with: "evidence_tier": "T1-T5", "severity": "critical|warning|info", "ease": "S|M|L", - "fix_skill": "/skill-name" + "fix_skill": "/idstack:" } ], "recommendations": [ @@ -742,13 +733,12 @@ Populate the `quality_review` section with: "fix": "..." } ] - } } ``` -When writing the manifest: +When building the payload: - Populate ALL fields in the `quality_review` section from the analysis above. -- Update the top-level `updated` timestamp to reflect the current time. +- The merge tool updates the top-level `updated` timestamp automatically. ### Score Trending Display @@ -768,6 +758,7 @@ One source of truth per data point. After writing the manifest, generate a shareable HTML report. The report follows the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +{{IDSTACK_RESOLVE}} # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -828,6 +819,7 @@ Include the overall_score so the preamble's context recovery can display score t across sessions (e.g., "Quality score trend: 62 -> 72 -> 78 over 3 reviews"). ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"course-quality-review","event":"completed","score":OVERALL_SCORE,"dimensions":{"teaching_presence":TP,"social_presence":SP,"cognitive_presence":CP}}' ``` @@ -838,5 +830,6 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"course-quality-review","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/learn/SKILL.md b/skills/learn/SKILL.md index 47641f9..324bd54 100644 --- a/skills/learn/SKILL.md +++ b/skills/learn/SKILL.md @@ -45,23 +45,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -73,6 +76,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -142,6 +159,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -175,7 +206,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -183,9 +216,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -243,11 +278,28 @@ Parse the user's intent and map to one of these commands: ### list (default) -Show the most recent learnings. If the user just said `/learn` with no arguments, +Show the most recent learnings. If the user just said `/idstack:learn` (Codex: `$learn`) with no arguments, this is the default. ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-search" --limit 10 ``` @@ -265,13 +317,47 @@ bloom-verbs | pedagogical | Avoid "understand" as ILO verb | 9/10 Search learnings by keyword. Supports `--cross-project` to include global learnings. ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-search" --keyword KEYWORD --limit 10 ``` For cross-project search: ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-search" --keyword KEYWORD --cross-project --limit 10 ``` @@ -282,7 +368,24 @@ If results include global learnings (tagged with `_source`), show their source p Delete a learning by its key. Always confirm with the user before deleting. ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-delete" KEY ``` @@ -291,7 +394,24 @@ for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" Copy a local learning to the global store so it's available across projects. ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-promote" KEY ``` @@ -300,7 +420,24 @@ for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" Export all learnings to a markdown file. ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-search" --limit 1000 ``` diff --git a/skills/learn/SKILL.md.tmpl b/skills/learn/SKILL.md.tmpl index 7f423b5..682db43 100644 --- a/skills/learn/SKILL.md.tmpl +++ b/skills/learn/SKILL.md.tmpl @@ -27,11 +27,11 @@ Parse the user's intent and map to one of these commands: ### list (default) -Show the most recent learnings. If the user just said `/learn` with no arguments, +Show the most recent learnings. If the user just said `/idstack:learn` (Codex: `$learn`) with no arguments, this is the default. ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-learnings-search" --limit 10 ``` @@ -49,13 +49,13 @@ bloom-verbs | pedagogical | Avoid "understand" as ILO verb | 9/10 Search learnings by keyword. Supports `--cross-project` to include global learnings. ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-learnings-search" --keyword KEYWORD --limit 10 ``` For cross-project search: ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-learnings-search" --keyword KEYWORD --cross-project --limit 10 ``` @@ -66,7 +66,7 @@ If results include global learnings (tagged with `_source`), show their source p Delete a learning by its key. Always confirm with the user before deleting. ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-learnings-delete" KEY ``` @@ -75,7 +75,7 @@ for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" Copy a local learning to the global store so it's available across projects. ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-learnings-promote" KEY ``` @@ -84,7 +84,7 @@ for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" Export all learnings to a markdown file. ```bash -for _p in "$CLAUDE_PLUGIN_ROOT" "$IDSTACK_HOME" "$HOME/.claude/plugins/idstack" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack"; do [ -n "$_p" ] && [ -d "$_p" ] && _IDSTACK="$_p" && break; done; : "${_IDSTACK:=$HOME/.claude/plugins/idstack}" +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-learnings-search" --limit 1000 ``` diff --git a/skills/learning-objectives/SKILL.md b/skills/learning-objectives/SKILL.md index 307222e..146393e 100644 --- a/skills/learning-objectives/SKILL.md +++ b/skills/learning-objectives/SKILL.md @@ -49,23 +49,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -77,6 +80,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -146,6 +163,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -179,7 +210,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -187,9 +220,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -252,7 +287,7 @@ Key findings encoded as decision rules in this skill: - **Constructive alignment improves student outcomes.** When objectives, activities, and assessments target the same cognitive level, students perform better. Misalignment is - one of the most common and most fixable problems in course design [Alignment-1] + one of the most common and most fixable problems in course design [Alignment-1] [T5] [Alignment-10] [T2]. - **Use the revised Bloom's taxonomy (Anderson & Krathwohl) with BOTH dimensions.** @@ -286,33 +321,17 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest +## Manifest Inputs (skill-specific) -Before starting objective development, check for an existing project manifest. - -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` - -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to the - user, offer to fix it, and STOP until it is valid. Never silently overwrite corrupt JSON. -- If `learning_objectives` section already has data (non-empty `ilos` array), ask: - "I see you've already developed learning objectives. Want to update them or start fresh?" -- Preserve all existing sections when writing back. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. **If NO_MANIFEST:** -- Say: "I notice you haven't run `/needs-analysis` yet. Running it first gives me your +- Say: "I notice you haven't run `/idstack:needs-analysis` yet. Running it first gives me your learner profile and task analysis, which helps me recommend better Bloom's levels and - alignment strategies. Want to continue anyway, or run `/needs-analysis` first?" + alignment strategies. Want to continue anyway, or run `/idstack:needs-analysis` first?" - If the user wants to continue, proceed without manifest context. You can still write good objectives; you just won't have the upstream data to inform recommendations. -- You will create the manifest at the end of this skill's workflow. --- @@ -456,7 +475,7 @@ Record any flags in the `expertise_reversal_flags` array for the manifest. This is the core value of this skill. Constructive alignment means every ILO connects to both a learning activity AND an assessment, and all three target the same cognitive level -[Alignment-1] [Alignment-10] [T2]. +[Alignment-1] [T5] [Alignment-10] [T2]. ### Forward Pass: ILO to Activity @@ -549,6 +568,24 @@ Then list: Before writing the manifest, generate an HTML report so the designer has a single document to read. The report follows the **visual contract** in `templates/report.html.tmpl` (the skeleton) and the **content contract** in `templates/report-format.md` (severity ordering, citation format, what each placeholder must carry). ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -566,6 +603,7 @@ Write the HTML report at the path printed above (`.idstack/exports/ - **Summary:** 2–3 sentences — how many ILOs you have, how many are well-aligned, the single most important gap or mismatch the designer should know about. - **Skill-specific section before Findings** — add a `
` with `

Alignment table

` and an HTML `
` (columns: ID, Objective, Knowledge, Process, Activity, Assessment, Alignment). Alignment values: `aligned` / `MISMATCH` / `GAP`. - **Finding ids:** `align-1`, `bloom-1`, `expertise-1`, etc. Findings come from bidirectional alignment gaps, Bloom's-level mismatches, expertise-reversal flags, and ambiguous verbs that were clarified. +- **Top recommendations:** the 3-5 highest-impact alignment fixes, ordered by leverage; cite each ([Domain-N] [TN]) and reference the finding id it addresses. - **Limitations:** alignment is read from manifest descriptions, not from the actual rubric criteria; expertise-reversal flags are inferred from the learner profile without a learner survey. - **Next steps:** Run `/idstack:assessment-design` to design assessments aligned to these objectives with evidence-based rubrics and feedback strategies. @@ -577,17 +615,58 @@ Every finding in the HTML must correspond to an entry in `learning_objectives.al Create or update the project manifest at `.idstack/project.json`. -**CRITICAL — Manifest Integrity Rules:** -1. If a manifest already exists, READ it first, then modify ONLY the `learning_objectives` - section. Preserve all other sections unchanged. -2. Include the COMPLETE schema structure. Do not omit fields. -3. Before writing, mentally verify the JSON is valid: matching braces, proper commas, - quoted strings, no trailing commas. -4. The `updated` timestamp must reflect the current time. -5. Set `learning_objectives.report_path` to the value of `$_REPORT_PATH` from the bash block above — i.e., `.idstack/exports//learning-objectives.html`. -6. If this is a new manifest (no needs analysis was run), initialize ALL sections - (including `needs_analysis`, `context`, and `quality_review`) with empty/default - values so downstream skills find the expected structure. +**If MANIFEST_EXISTS (primary path):** write the `learning_objectives` section via +`bin/idstack-manifest-merge`. The merge tool replaces only the named section, preserves +every other section verbatim, validates JSON, writes atomically (tempfile + rename), and +updates the top-level `updated` timestamp for you: + +```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done +"$_IDSTACK/bin/idstack-manifest-merge" --section learning_objectives --payload - <<'PAYLOAD' +{ + "report_path": "/learning-objectives.html>", + "ilos": [], + "alignment_matrix": { + "ilo_to_activity": {}, + "ilo_to_assessment": {}, + "gaps": [] + }, + "expertise_reversal_flags": [] +} +PAYLOAD +``` + +Replace the placeholder payload above with the actual session data. Include the COMPLETE +section structure — do not omit fields. If the tool exits non-zero (e.g., exit 4 = manifest +not found, exit 2 = malformed manifest), report the error to the user and stop; never +silently overwrite. + +**If NO_MANIFEST (first run only):** use the Write tool to create the full manifest — +first-run init writes the whole document; the merge tool replaces exactly one section per +call. Initialize ALL sections (including `needs_analysis`, `context`, and `quality_review`) +with empty/default values per the schema below so downstream skills find the expected +structure, set the `updated` timestamp to the current time, and verify the JSON is valid +before writing. + +In both paths, set `learning_objectives.report_path` to the value of `$_REPORT_PATH` from +the bash block above — i.e., `.idstack/exports//learning-objectives.html`. **Populate the `learning_objectives` section:** @@ -614,7 +693,7 @@ Write the manifest, then confirm to the user: read. Open it in any browser; the folder is self-contained. - System state: `.idstack/project.json` (the manifest — for downstream skills). -**Next step:** Run `/assessment-design` to design assessments aligned to your objectives +**Next step:** Run `/idstack:assessment-design` to design assessments aligned to your objectives with evidence-based rubrics and feedback strategies." --- @@ -632,7 +711,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -643,10 +722,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1021,6 +1122,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"learning-objectives","event":"completed"}' ``` @@ -1031,5 +1150,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"learning-objectives","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/learning-objectives/SKILL.md.tmpl b/skills/learning-objectives/SKILL.md.tmpl index 9bf7972..8415f7f 100644 --- a/skills/learning-objectives/SKILL.md.tmpl +++ b/skills/learning-objectives/SKILL.md.tmpl @@ -36,7 +36,7 @@ Key findings encoded as decision rules in this skill: - **Constructive alignment improves student outcomes.** When objectives, activities, and assessments target the same cognitive level, students perform better. Misalignment is - one of the most common and most fixable problems in course design [Alignment-1] + one of the most common and most fixable problems in course design [Alignment-1] [T5] [Alignment-10] [T2]. - **Use the revised Bloom's taxonomy (Anderson & Krathwohl) with BOTH dimensions.** @@ -70,33 +70,17 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest +## Manifest Inputs (skill-specific) -Before starting objective development, check for an existing project manifest. - -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` - -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to the - user, offer to fix it, and STOP until it is valid. Never silently overwrite corrupt JSON. -- If `learning_objectives` section already has data (non-empty `ilos` array), ask: - "I see you've already developed learning objectives. Want to update them or start fresh?" -- Preserve all existing sections when writing back. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. **If NO_MANIFEST:** -- Say: "I notice you haven't run `/needs-analysis` yet. Running it first gives me your +- Say: "I notice you haven't run `/idstack:needs-analysis` yet. Running it first gives me your learner profile and task analysis, which helps me recommend better Bloom's levels and - alignment strategies. Want to continue anyway, or run `/needs-analysis` first?" + alignment strategies. Want to continue anyway, or run `/idstack:needs-analysis` first?" - If the user wants to continue, proceed without manifest context. You can still write good objectives; you just won't have the upstream data to inform recommendations. -- You will create the manifest at the end of this skill's workflow. --- @@ -240,7 +224,7 @@ Record any flags in the `expertise_reversal_flags` array for the manifest. This is the core value of this skill. Constructive alignment means every ILO connects to both a learning activity AND an assessment, and all three target the same cognitive level -[Alignment-1] [Alignment-10] [T2]. +[Alignment-1] [T5] [Alignment-10] [T2]. ### Forward Pass: ILO to Activity @@ -333,6 +317,7 @@ Then list: Before writing the manifest, generate an HTML report so the designer has a single document to read. The report follows the **visual contract** in `templates/report.html.tmpl` (the skeleton) and the **content contract** in `templates/report-format.md` (severity ordering, citation format, what each placeholder must carry). ```bash +{{IDSTACK_RESOLVE}} # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -350,6 +335,7 @@ Write the HTML report at the path printed above (`.idstack/exports/ - **Summary:** 2–3 sentences — how many ILOs you have, how many are well-aligned, the single most important gap or mismatch the designer should know about. - **Skill-specific section before Findings** — add a `
` with `

Alignment table

` and an HTML `
` (columns: ID, Objective, Knowledge, Process, Activity, Assessment, Alignment). Alignment values: `aligned` / `MISMATCH` / `GAP`. - **Finding ids:** `align-1`, `bloom-1`, `expertise-1`, etc. Findings come from bidirectional alignment gaps, Bloom's-level mismatches, expertise-reversal flags, and ambiguous verbs that were clarified. +- **Top recommendations:** the 3-5 highest-impact alignment fixes, ordered by leverage; cite each ([Domain-N] [TN]) and reference the finding id it addresses. - **Limitations:** alignment is read from manifest descriptions, not from the actual rubric criteria; expertise-reversal flags are inferred from the learner profile without a learner survey. - **Next steps:** Run `/idstack:assessment-design` to design assessments aligned to these objectives with evidence-based rubrics and feedback strategies. @@ -361,17 +347,41 @@ Every finding in the HTML must correspond to an entry in `learning_objectives.al Create or update the project manifest at `.idstack/project.json`. -**CRITICAL — Manifest Integrity Rules:** -1. If a manifest already exists, READ it first, then modify ONLY the `learning_objectives` - section. Preserve all other sections unchanged. -2. Include the COMPLETE schema structure. Do not omit fields. -3. Before writing, mentally verify the JSON is valid: matching braces, proper commas, - quoted strings, no trailing commas. -4. The `updated` timestamp must reflect the current time. -5. Set `learning_objectives.report_path` to the value of `$_REPORT_PATH` from the bash block above — i.e., `.idstack/exports//learning-objectives.html`. -6. If this is a new manifest (no needs analysis was run), initialize ALL sections - (including `needs_analysis`, `context`, and `quality_review`) with empty/default - values so downstream skills find the expected structure. +**If MANIFEST_EXISTS (primary path):** write the `learning_objectives` section via +`bin/idstack-manifest-merge`. The merge tool replaces only the named section, preserves +every other section verbatim, validates JSON, writes atomically (tempfile + rename), and +updates the top-level `updated` timestamp for you: + +```bash +{{IDSTACK_RESOLVE}} +"$_IDSTACK/bin/idstack-manifest-merge" --section learning_objectives --payload - <<'PAYLOAD' +{ + "report_path": "/learning-objectives.html>", + "ilos": [], + "alignment_matrix": { + "ilo_to_activity": {}, + "ilo_to_assessment": {}, + "gaps": [] + }, + "expertise_reversal_flags": [] +} +PAYLOAD +``` + +Replace the placeholder payload above with the actual session data. Include the COMPLETE +section structure — do not omit fields. If the tool exits non-zero (e.g., exit 4 = manifest +not found, exit 2 = malformed manifest), report the error to the user and stop; never +silently overwrite. + +**If NO_MANIFEST (first run only):** use the Write tool to create the full manifest — +first-run init writes the whole document; the merge tool replaces exactly one section per +call. Initialize ALL sections (including `needs_analysis`, `context`, and `quality_review`) +with empty/default values per the schema below so downstream skills find the expected +structure, set the `updated` timestamp to the current time, and verify the JSON is valid +before writing. + +In both paths, set `learning_objectives.report_path` to the value of `$_REPORT_PATH` from +the bash block above — i.e., `.idstack/exports//learning-objectives.html`. **Populate the `learning_objectives` section:** @@ -398,7 +408,7 @@ Write the manifest, then confirm to the user: read. Open it in any browser; the folder is self-contained. - System state: `.idstack/project.json` (the manifest — for downstream skills). -**Next step:** Run `/assessment-design` to design assessments aligned to your objectives +**Next step:** Run `/idstack:assessment-design` to design assessments aligned to your objectives with evidence-based rubrics and feedback strategies." --- @@ -416,6 +426,7 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"learning-objectives","event":"completed"}' ``` @@ -426,5 +437,6 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"learning-objectives","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/needs-analysis/SKILL.md b/skills/needs-analysis/SKILL.md index 00c3f83..74d402c 100644 --- a/skills/needs-analysis/SKILL.md +++ b/skills/needs-analysis/SKILL.md @@ -49,23 +49,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -77,6 +80,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -146,6 +163,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -179,7 +210,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -187,9 +220,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -270,28 +305,10 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest - -Before starting the needs assessment, check for an existing project manifest. - -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` - -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to the - user, offer to fix it, and STOP until it is valid. Never silently overwrite corrupt JSON. -- If `needs_analysis` section already has data, ask: "I see you've already run a needs - analysis. Want to update it or start fresh?" -- Preserve all existing sections when writing back. +## Manifest Inputs (skill-specific) -**If NO_MANIFEST:** -- You will create the manifest at the end of this skill's workflow. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. --- @@ -553,6 +570,24 @@ Based on the learner profile, note which instructional strategies are appropriat Generate an HTML report so the designer has a single document to read. The report follows the **visual contract** in `templates/report.html.tmpl` (the skeleton) and the **content contract** in `templates/report-format.md` (severity ordering, citation format, what each placeholder must carry). ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -583,6 +618,8 @@ Every finding in the HTML must correspond to a finding the manifest's `needs_ana Create or update the project manifest. Use the Write tool to write `.idstack/project.json`. +**Why this skill uses the Write-tool fallback (not `bin/idstack-manifest-merge`):** needs-analysis writes top-level fields (`project_name`, `created`), the `context` section, AND the `needs_analysis` section — and on a first run it initializes ALL sections so downstream skills find the expected structure. The merge tool replaces exactly one section per call, so this skill uses the Read-modify-Write path described in `templates/manifest-schema.md`. When a manifest already exists, preserve every other section unchanged. + **CRITICAL — Manifest Integrity Rules:** 1. If a manifest already exists, READ it first, then modify only the sections this skill owns (context, needs_analysis). Preserve all other sections unchanged. @@ -605,7 +642,7 @@ Write the manifest, then confirm to the user: (CSS is bundled), so you can zip or email it. - System state: `.idstack/project.json` (the manifest — for downstream skills). -**Next step:** Run `/learning-objectives` to develop learning objectives based on +**Next step:** Run `/idstack:learning-objectives` to develop learning objectives based on this analysis. The objectives skill will read your task analysis and learner profile to recommend appropriate Bloom's levels and alignment strategies." @@ -624,7 +661,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -635,10 +672,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. @@ -1013,6 +1072,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"needs-analysis","event":"completed"}' ``` @@ -1023,5 +1100,23 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"needs-analysis","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/needs-analysis/SKILL.md.tmpl b/skills/needs-analysis/SKILL.md.tmpl index 9e2ed9e..8198cb9 100644 --- a/skills/needs-analysis/SKILL.md.tmpl +++ b/skills/needs-analysis/SKILL.md.tmpl @@ -54,28 +54,10 @@ When multiple tiers apply, cite the strongest. --- -## Preamble: Project Manifest +## Manifest Inputs (skill-specific) -Before starting the needs assessment, check for an existing project manifest. - -```bash -if [ -f ".idstack/project.json" ]; then - echo "MANIFEST_EXISTS" - "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json -else - echo "NO_MANIFEST" -fi -``` - -**If MANIFEST_EXISTS:** -- Read the manifest. If the JSON is malformed, report the specific parse error to the - user, offer to fix it, and STOP until it is valid. Never silently overwrite corrupt JSON. -- If `needs_analysis` section already has data, ask: "I see you've already run a needs - analysis. Want to update it or start fresh?" -- Preserve all existing sections when writing back. - -**If NO_MANIFEST:** -- You will create the manifest at the end of this skill's workflow. +The shared preamble above already ran the manifest existence check +(`MANIFEST_EXISTS` / `NO_MANIFEST`) and this skill's re-run question. --- @@ -337,6 +319,7 @@ Based on the learner profile, note which instructional strategies are appropriat Generate an HTML report so the designer has a single document to read. The report follows the **visual contract** in `templates/report.html.tmpl` (the skeleton) and the **content contract** in `templates/report-format.md` (severity ordering, citation format, what each placeholder must carry). ```bash +{{IDSTACK_RESOLVE}} # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -367,6 +350,8 @@ Every finding in the HTML must correspond to a finding the manifest's `needs_ana Create or update the project manifest. Use the Write tool to write `.idstack/project.json`. +**Why this skill uses the Write-tool fallback (not `bin/idstack-manifest-merge`):** needs-analysis writes top-level fields (`project_name`, `created`), the `context` section, AND the `needs_analysis` section — and on a first run it initializes ALL sections so downstream skills find the expected structure. The merge tool replaces exactly one section per call, so this skill uses the Read-modify-Write path described in `templates/manifest-schema.md`. When a manifest already exists, preserve every other section unchanged. + **CRITICAL — Manifest Integrity Rules:** 1. If a manifest already exists, READ it first, then modify only the sections this skill owns (context, needs_analysis). Preserve all other sections unchanged. @@ -389,7 +374,7 @@ Write the manifest, then confirm to the user: (CSS is bundled), so you can zip or email it. - System state: `.idstack/project.json` (the manifest — for downstream skills). -**Next step:** Run `/learning-objectives` to develop learning objectives based on +**Next step:** Run `/idstack:learning-objectives` to develop learning objectives based on this analysis. The objectives skill will read your task analysis and learner profile to recommend appropriate Bloom's levels and alignment strategies." @@ -408,6 +393,7 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"needs-analysis","event":"completed"}' ``` @@ -418,5 +404,6 @@ If you discover a non-obvious project-specific quirk during this session (LMS be import format issue, course structure pattern), also log it as a learning: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"needs-analysis","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/pipeline/SKILL.md b/skills/pipeline/SKILL.md index 1238de8..6cc6362 100644 --- a/skills/pipeline/SKILL.md +++ b/skills/pipeline/SKILL.md @@ -49,23 +49,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -77,6 +80,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -146,6 +163,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -179,7 +210,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -187,9 +220,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -244,19 +279,19 @@ skipping skills that have already been completed. The canonical pipeline order is: ``` -1. /needs-analysis — Three-level needs assessment -2. /learning-objectives — Evidence-based ILO development -3. /assessment-design — Assessment & rubric design -4. /course-builder — Generate course content -5. /course-quality-review — Quality audit (QM + CoI) -6. /accessibility-review — WCAG + UDL review -7. /red-team — Adversarial audit -8. /course-export — Package for LMS +1. /idstack:needs-analysis — Three-level needs assessment +2. /idstack:learning-objectives — Evidence-based ILO development +3. /idstack:assessment-design — Assessment & rubric design +4. /idstack:course-builder — Generate course content +5. /idstack:course-quality-review — Quality audit (QM + CoI) +6. /idstack:accessibility-review — WCAG + UDL review +7. /idstack:red-team — Adversarial audit +8. /idstack:course-export — Package for LMS ``` -**Alternative entry point:** If the user has run `/course-import` (visible in timeline), -the pipeline starts at `/learning-objectives` (skipping /needs-analysis, since the import -populated the manifest with equivalent data). +**Alternative entry point:** If the user has run `/idstack:course-import` (visible in +timeline), the pipeline starts at `/idstack:learning-objectives` (skipping +`/idstack:needs-analysis`, since the import populated the manifest with equivalent data). ## Determining Completed Skills @@ -313,17 +348,17 @@ Show the user a status table before starting: ``` Pipeline Status: - [done] /needs-analysis - [done] /learning-objectives - [next] /assessment-design <-- starting here - [ ] /course-builder - [ ] /course-quality-review - [ ] /accessibility-review - [ ] /red-team - [ ] /course-export + [done] /idstack:needs-analysis + [done] /idstack:learning-objectives + [next] /idstack:assessment-design <-- starting here + [ ] /idstack:course-builder + [ ] /idstack:course-quality-review + [ ] /idstack:accessibility-review + [ ] /idstack:red-team + [ ] /idstack:course-export ``` -Ask: "Ready to continue the pipeline from /assessment-design?" (using AskUserQuestion +Ask: "Ready to continue the pipeline from /idstack:assessment-design?" (using AskUserQuestion with options: "Yes, continue" / "Start from a different skill" / "Re-run a completed skill") If the user picks "Start from a different skill" or "Re-run a completed skill", @@ -333,8 +368,10 @@ ask which one using AskUserQuestion with the skill list as options. For each skill from the starting point onward: -1. Announce: "Starting /skill-name..." -2. Invoke the skill using the `Skill` tool with the skill name (e.g., `skill: "needs-analysis"`) +1. Announce: "Starting /idstack:..." +2. Invoke the skill using the `Skill` tool with the **plugin-namespaced** skill name + (e.g., `skill: "idstack:needs-analysis"`). Plugin skills are addressed as + `plugin:skill` — a bare `"needs-analysis"` does not resolve in Claude Code. 3. The skill will run its full workflow including all AskUserQuestion interactions 4. When the skill completes (logs to timeline), **execute Step 4 (Generate Course Dashboard) inline** to refresh `.idstack/exports//index.html` against the new per-skill report. This keeps the dashboard fresh if the designer pauses partway through. 5. Announce completion and move to next. @@ -348,7 +385,7 @@ reading the timeline and continuing from the new starting point. Generate the co dashboard (Step 4) before stopping so the partial-run dashboard is up to date. **Between skills**, briefly announce the transition: -"[skill-name] complete. Course dashboard refreshed. Moving to /next-skill..." +"[skill-name] complete. Course dashboard refreshed. Moving to /idstack:..." ### Step 4: Generate Course Dashboard @@ -362,6 +399,24 @@ After each skill completes (or after the orchestrator finishes the run, includin **Prep.** Compute the course slug and prepare the export folder: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") _EXPORT_DIR=".idstack/exports/$_SLUG" @@ -402,8 +457,31 @@ The dashboard is overwritten on every pipeline run — historical dashboards are When all remaining skills have been executed: - Announce: "Pipeline complete. Your course has been through all 8 stages." - Confirm the dashboard path: "Course dashboard at `.idstack/exports//index.html` — open it in any browser for the cross-cutting view; the per-skill HTML reports in the same folder carry the full detail. Zip the folder to hand it to a stakeholder." -- If `/course-quality-review` produced a score, show it. +- If `/idstack:course-quality-review` produced a score, show it. - Remind the user they can re-run any skill individually if needed, and that re-running `/idstack:pipeline` regenerates the course dashboard. +- Log the pipeline run so "the pipeline was run" is recoverable from the timeline: + +```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done +[ -n "$_IDSTACK" ] && "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"pipeline","event":"completed"}' +``` ## Important Rules @@ -413,6 +491,6 @@ When all remaining skills have been executed: questions or commentary between the skill's AskUserQuestion prompts. - **Respect the user.** If at any point the user says "stop", "pause", or "that's enough for now", stop the pipeline gracefully. Their progress is saved in - timeline.jsonl and they can resume later with `/idstack pipeline`. + timeline.jsonl and they can resume later with `/idstack:pipeline`. - **No quality gate yet.** In v2.0, skip logic is purely completion-based. Quality-gated skipping (only skip if score > threshold) is planned for a future version. diff --git a/skills/pipeline/SKILL.md.tmpl b/skills/pipeline/SKILL.md.tmpl index 30cfdbf..5de391e 100644 --- a/skills/pipeline/SKILL.md.tmpl +++ b/skills/pipeline/SKILL.md.tmpl @@ -28,19 +28,19 @@ skipping skills that have already been completed. The canonical pipeline order is: ``` -1. /needs-analysis — Three-level needs assessment -2. /learning-objectives — Evidence-based ILO development -3. /assessment-design — Assessment & rubric design -4. /course-builder — Generate course content -5. /course-quality-review — Quality audit (QM + CoI) -6. /accessibility-review — WCAG + UDL review -7. /red-team — Adversarial audit -8. /course-export — Package for LMS +1. /idstack:needs-analysis — Three-level needs assessment +2. /idstack:learning-objectives — Evidence-based ILO development +3. /idstack:assessment-design — Assessment & rubric design +4. /idstack:course-builder — Generate course content +5. /idstack:course-quality-review — Quality audit (QM + CoI) +6. /idstack:accessibility-review — WCAG + UDL review +7. /idstack:red-team — Adversarial audit +8. /idstack:course-export — Package for LMS ``` -**Alternative entry point:** If the user has run `/course-import` (visible in timeline), -the pipeline starts at `/learning-objectives` (skipping /needs-analysis, since the import -populated the manifest with equivalent data). +**Alternative entry point:** If the user has run `/idstack:course-import` (visible in +timeline), the pipeline starts at `/idstack:learning-objectives` (skipping +`/idstack:needs-analysis`, since the import populated the manifest with equivalent data). ## Determining Completed Skills @@ -97,17 +97,17 @@ Show the user a status table before starting: ``` Pipeline Status: - [done] /needs-analysis - [done] /learning-objectives - [next] /assessment-design <-- starting here - [ ] /course-builder - [ ] /course-quality-review - [ ] /accessibility-review - [ ] /red-team - [ ] /course-export + [done] /idstack:needs-analysis + [done] /idstack:learning-objectives + [next] /idstack:assessment-design <-- starting here + [ ] /idstack:course-builder + [ ] /idstack:course-quality-review + [ ] /idstack:accessibility-review + [ ] /idstack:red-team + [ ] /idstack:course-export ``` -Ask: "Ready to continue the pipeline from /assessment-design?" (using AskUserQuestion +Ask: "Ready to continue the pipeline from /idstack:assessment-design?" (using AskUserQuestion with options: "Yes, continue" / "Start from a different skill" / "Re-run a completed skill") If the user picks "Start from a different skill" or "Re-run a completed skill", @@ -117,8 +117,10 @@ ask which one using AskUserQuestion with the skill list as options. For each skill from the starting point onward: -1. Announce: "Starting /skill-name..." -2. Invoke the skill using the `Skill` tool with the skill name (e.g., `skill: "needs-analysis"`) +1. Announce: "Starting /idstack:..." +2. Invoke the skill using the `Skill` tool with the **plugin-namespaced** skill name + (e.g., `skill: "idstack:needs-analysis"`). Plugin skills are addressed as + `plugin:skill` — a bare `"needs-analysis"` does not resolve in Claude Code. 3. The skill will run its full workflow including all AskUserQuestion interactions 4. When the skill completes (logs to timeline), **execute Step 4 (Generate Course Dashboard) inline** to refresh `.idstack/exports//index.html` against the new per-skill report. This keeps the dashboard fresh if the designer pauses partway through. 5. Announce completion and move to next. @@ -132,7 +134,7 @@ reading the timeline and continuing from the new starting point. Generate the co dashboard (Step 4) before stopping so the partial-run dashboard is up to date. **Between skills**, briefly announce the transition: -"[skill-name] complete. Course dashboard refreshed. Moving to /next-skill..." +"[skill-name] complete. Course dashboard refreshed. Moving to /idstack:..." ### Step 4: Generate Course Dashboard @@ -146,6 +148,7 @@ After each skill completes (or after the orchestrator finishes the run, includin **Prep.** Compute the course slug and prepare the export folder: ```bash +{{IDSTACK_RESOLVE}} _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") _EXPORT_DIR=".idstack/exports/$_SLUG" @@ -186,8 +189,14 @@ The dashboard is overwritten on every pipeline run — historical dashboards are When all remaining skills have been executed: - Announce: "Pipeline complete. Your course has been through all 8 stages." - Confirm the dashboard path: "Course dashboard at `.idstack/exports//index.html` — open it in any browser for the cross-cutting view; the per-skill HTML reports in the same folder carry the full detail. Zip the folder to hand it to a stakeholder." -- If `/course-quality-review` produced a score, show it. +- If `/idstack:course-quality-review` produced a score, show it. - Remind the user they can re-run any skill individually if needed, and that re-running `/idstack:pipeline` regenerates the course dashboard. +- Log the pipeline run so "the pipeline was run" is recoverable from the timeline: + +```bash +{{IDSTACK_RESOLVE}} +[ -n "$_IDSTACK" ] && "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"pipeline","event":"completed"}' +``` ## Important Rules @@ -197,6 +206,6 @@ When all remaining skills have been executed: questions or commentary between the skill's AskUserQuestion prompts. - **Respect the user.** If at any point the user says "stop", "pause", or "that's enough for now", stop the pipeline gracefully. Their progress is saved in - timeline.jsonl and they can resume later with `/idstack pipeline`. + timeline.jsonl and they can resume later with `/idstack:pipeline`. - **No quality gate yet.** In v2.0, skip logic is purely completion-based. Quality-gated skipping (only skip if score > threshold) is planned for a future version. diff --git a/skills/red-team/SKILL.md b/skills/red-team/SKILL.md index 11bf1af..f8fa417 100644 --- a/skills/red-team/SKILL.md +++ b/skills/red-team/SKILL.md @@ -52,23 +52,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -80,6 +83,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -149,6 +166,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -182,7 +213,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -190,9 +223,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), @@ -280,7 +315,7 @@ No automatic re-verification. If the user wants to confirm fixes hold, they re-r The preamble above already ran the manifest check. Now confirm scope. **Determine course inputs:** -- If `MANIFEST_EXISTS`: the orchestrator will read all sections (`needs_analysis`, `learning_objectives`, `assessment_design`, `course_builder`, `quality_review`, `accessibility_review`). +- If `MANIFEST_EXISTS`: the orchestrator will read all sections (`needs_analysis`, `learning_objectives`, `assessments`, `course_content`, `quality_review`, `accessibility_review`). - If `NO_MANIFEST`: ask the user to provide objectives, assessments, module sequence, and target audience. Capture answers as a brief block to pass to the orchestrator. Standalone mode reduces precision on Dimensions 1 (alignment) and 5 (prerequisites). **Ask one focus question** via AskUserQuestion: @@ -302,7 +337,7 @@ Use the **Agent tool** with `subagent_type=general-purpose`. The prompt is the f - `{{FOCUS}}` → the user's choice from Step 1 (or `Full sweep` by default) - `{{MANIFEST_INFO}}` → either `Manifest at .idstack/project.json — read it directly.` or, in standalone mode, the captured course information from Step 1 -- `{{COURSE_FILES_HINT}}` → if the manifest has `course_builder.output_path`, set this to that path; otherwise `Look under ./course/ or ./modules/ for generated course files.` +- `{{COURSE_FILES_HINT}}` → if the manifest has `course_content.content_dir`, set this to that path; otherwise `Look under ./course/ or ./modules/ for generated course files.` Then call Agent. Block on its return. @@ -506,6 +541,24 @@ Contextualize: The orchestrator emits an HTML report. Follow the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -585,6 +638,24 @@ the orchestrator's HTML report at `$_REPORT_PATH` (the report is the source of t not re-derive from the orchestrator's return summary, which is lossy). ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload - <<'PAYLOAD' { "updated": "", @@ -615,8 +686,31 @@ Each finding object: `{"id": "alignment-1", "description": "...", "module": "Mod The merge tool exits non-zero (and prints a diagnostic on stderr) if the payload is malformed, the manifest is corrupt, or the section name is misspelled — never silently overwriting. If -`.idstack/project.json` doesn't exist yet, run `bin/idstack-migrate .idstack/project.json` -first (it creates a fresh canonical manifest). +`.idstack/project.json` doesn't exist yet (standalone mode), create it first with `--init`, +which writes a canonical manifest with every section at its default — without one the merge +tool exits 4: + +```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done +"$_IDSTACK/bin/idstack-migrate" --init .idstack/project.json >/dev/null +``` **Fallback (if `bin/idstack-manifest-merge` is unavailable):** Read the full manifest, modify only the `red_team_audit` section, Write back. Preserve all other sections verbatim. The @@ -641,6 +735,24 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"red-team","event":"completed"}' ``` @@ -649,5 +761,23 @@ Include skill-specific fields where available (confidence_score, focus, fixes_ap If you discover a non-obvious project-specific quirk during this session (LMS behavior, import format issue, course structure pattern), also log it as a learning: ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"red-team","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/skills/red-team/SKILL.md.tmpl b/skills/red-team/SKILL.md.tmpl index 3b839bd..c25e873 100644 --- a/skills/red-team/SKILL.md.tmpl +++ b/skills/red-team/SKILL.md.tmpl @@ -64,7 +64,7 @@ No automatic re-verification. If the user wants to confirm fixes hold, they re-r The preamble above already ran the manifest check. Now confirm scope. **Determine course inputs:** -- If `MANIFEST_EXISTS`: the orchestrator will read all sections (`needs_analysis`, `learning_objectives`, `assessment_design`, `course_builder`, `quality_review`, `accessibility_review`). +- If `MANIFEST_EXISTS`: the orchestrator will read all sections (`needs_analysis`, `learning_objectives`, `assessments`, `course_content`, `quality_review`, `accessibility_review`). - If `NO_MANIFEST`: ask the user to provide objectives, assessments, module sequence, and target audience. Capture answers as a brief block to pass to the orchestrator. Standalone mode reduces precision on Dimensions 1 (alignment) and 5 (prerequisites). **Ask one focus question** via AskUserQuestion: @@ -86,7 +86,7 @@ Use the **Agent tool** with `subagent_type=general-purpose`. The prompt is the f - `{{FOCUS}}` → the user's choice from Step 1 (or `Full sweep` by default) - `{{MANIFEST_INFO}}` → either `Manifest at .idstack/project.json — read it directly.` or, in standalone mode, the captured course information from Step 1 -- `{{COURSE_FILES_HINT}}` → if the manifest has `course_builder.output_path`, set this to that path; otherwise `Look under ./course/ or ./modules/ for generated course files.` +- `{{COURSE_FILES_HINT}}` → if the manifest has `course_content.content_dir`, set this to that path; otherwise `Look under ./course/ or ./modules/ for generated course files.` Then call Agent. Block on its return. @@ -290,6 +290,7 @@ Contextualize: The orchestrator emits an HTML report. Follow the **visual contract** in `templates/report.html.tmpl` and the **content contract** in `templates/report-format.md`. ```bash +{{IDSTACK_RESOLVE}} # Compute the course slug from project_name and prepare the export folder. _PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "") _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") @@ -369,6 +370,7 @@ the orchestrator's HTML report at `$_REPORT_PATH` (the report is the source of t not re-derive from the orchestrator's return summary, which is lossy). ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload - <<'PAYLOAD' { "updated": "", @@ -399,8 +401,14 @@ Each finding object: `{"id": "alignment-1", "description": "...", "module": "Mod The merge tool exits non-zero (and prints a diagnostic on stderr) if the payload is malformed, the manifest is corrupt, or the section name is misspelled — never silently overwriting. If -`.idstack/project.json` doesn't exist yet, run `bin/idstack-migrate .idstack/project.json` -first (it creates a fresh canonical manifest). +`.idstack/project.json` doesn't exist yet (standalone mode), create it first with `--init`, +which writes a canonical manifest with every section at its default — without one the merge +tool exits 4: + +```bash +{{IDSTACK_RESOLVE}} +"$_IDSTACK/bin/idstack-migrate" --init .idstack/project.json >/dev/null +``` **Fallback (if `bin/idstack-manifest-merge` is unavailable):** Read the full manifest, modify only the `red_team_audit` section, Write back. Preserve all other sections verbatim. The @@ -425,6 +433,7 @@ Have feedback or a feature request? [Share it here](https://forms.gle/6LDgDD1M6W After the skill workflow completes successfully, log the session to the timeline: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"red-team","event":"completed"}' ``` @@ -433,5 +442,6 @@ Include skill-specific fields where available (confidence_score, focus, fixes_ap If you discover a non-obvious project-specific quirk during this session (LMS behavior, import format issue, course structure pattern), also log it as a learning: ```bash +{{IDSTACK_RESOLVE}} "$_IDSTACK/bin/idstack-learnings-log" '{"skill":"red-team","type":"operational","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":8,"source":"observed"}' ``` diff --git a/templates/index.html.tmpl b/templates/index.html.tmpl index af3e4e8..cc66972 100644 --- a/templates/index.html.tmpl +++ b/templates/index.html.tmpl @@ -123,7 +123,16 @@ - + +
{{course_export_signal}} course-export.html
diff --git a/templates/manifest-schema.md b/templates/manifest-schema.md index fe70041..7775be8 100644 --- a/templates/manifest-schema.md +++ b/templates/manifest-schema.md @@ -11,7 +11,7 @@ Every skill that produces findings emits **both**: - a **JSON section** in this manifest (system state — read by other skills, the pipeline orchestrator, and `bin/idstack-status`), and - an **HTML report** at `.idstack/exports//.html` (the human view — read by the instructional designer). -The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills and tools can find it. +The HTML report follows the visual contract in `templates/report.html.tmpl` and the content contract in `templates/report-format.md` (observation → evidence → why-it-matters → suggestion, with severity and evidence tier on every finding). The skill writes the report's relative path back into its own section's `report_path` field so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the top-level `project_name` field via `bin/idstack-slugify` (rule: NFKD-fold, lowercase, kebab-case, ASCII-safe; empty input → `untitled-course`). The slug is computed deterministically — skills don't cache it in the manifest. All exports for a course — per-skill HTML reports, the pipeline dashboard at `index.html`, and LMS packages (`course-export.imscc`, `scorm-export.zip`) — live under the same `.idstack/exports//` folder so the deliverable is self-describing when zipped, emailed, or handed off. @@ -22,10 +22,32 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and **1. Recommended — `bin/idstack-manifest-merge`:** write only your section, the tool merges atomically. ```bash +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done # Write a payload for your skill's section, then: "$_IDSTACK/bin/idstack-manifest-merge" --section red_team_audit --payload /tmp/payload.json ``` +(This file is spliced into skills verbatim, so the resolution above is written +out rather than using the `IDSTACK_RESOLVE` placeholder — that placeholder is +only expanded in `SKILL.md.tmpl` bodies.) + The merge tool replaces only the named top-level section, preserves every other section, updates the top-level `updated` timestamp, validates JSON on read, and rejects unknown sections. Use this in preference to inlining the full manifest in `Edit` operations. **2. Fallback — manual full-manifest write:** if the merge tool is unavailable for some reason, Read the full manifest, modify only your section, Write back. Preserve all other sections verbatim. Use the full schema below as reference. diff --git a/templates/preamble.md b/templates/preamble.md index 993073a..213ddf4 100644 --- a/templates/preamble.md +++ b/templates/preamble.md @@ -29,23 +29,26 @@ These are **directives to the model**, not magic words — interpret them as the ## Preamble: Update Check ```bash -# Locate the idstack install. Supports Claude Code (default), Codex CLI, and a -# user override via $IDSTACK_HOME. -if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then - _IDSTACK="$CLAUDE_PLUGIN_ROOT" -elif [ -n "${IDSTACK_HOME:-}" ]; then - _IDSTACK="$IDSTACK_HOME" -elif [ -d "$HOME/.agents/plugins/idstack" ]; then - _IDSTACK="$HOME/.agents/plugins/idstack" -elif [ -d "$HOME/.agents/skills/idstack" ]; then - _IDSTACK="$HOME/.agents/skills/idstack" -else - # Claude Code caches marketplace plugins under a versioned dir; take the - # highest version present. Empty if idstack was never installed this way — - # every "$_IDSTACK/bin/..." call below is guarded, so that degrades quietly. - _IDSTACK=$(ls -d "$HOME"/.claude/plugins/cache/idstack/idstack/*/ 2>/dev/null | sort | tail -1) - _IDSTACK="${_IDSTACK%/}" +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache (highest version). Empty if none found; +# guard "$_IDSTACK/bin/..." calls accordingly. +# Canonical copy: templates/snippets/idstack-resolve.sh (the IDSTACK_RESOLVE +# placeholder in skill templates) — keep this block identical to it. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) [ -n "$_UPD" ] && echo "$_UPD" ``` @@ -57,6 +60,20 @@ If the output contains `UPDATE_AVAILABLE`: tell the user "A newer version of ids Before starting, check for an existing project manifest. ```bash +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done if [ -f ".idstack/project.json" ]; then echo "MANIFEST_EXISTS" "$_IDSTACK/bin/idstack-migrate" .idstack/project.json 2>/dev/null || cat .idstack/project.json @@ -126,6 +143,20 @@ Check for session history and learnings from prior runs. ```bash # Context recovery: timeline + learnings +# (fresh shell — re-derive the install dir; see Preamble: Update Check) +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done _HAS_TIMELINE=0 _HAS_LEARNINGS=0 if [ -f ".idstack/timeline.jsonl" ]; then @@ -159,7 +190,9 @@ completed = set() for e in events: if e.get('event') == 'completed': completed.add(e.get('skill', '')) -print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}') +# No f-string here: nesting same-type quotes in a replacement field is a +# SyntaxError before Python 3.12, and macOS system python3 is 3.9. +print('SKILLS_COMPLETED: ' + ','.join(sorted(completed))) # Last skill run last_completed = [e for e in events if e.get('event') == 'completed'] @@ -167,9 +200,11 @@ if last_completed: last = last_completed[-1] print(f'LAST_SKILL: {last.get(\"skill\",\"?\")} at {last.get(\"ts\",\"?\")}') -# Pipeline progression +# Pipeline progression. course-import is the alternative entry point — it +# joins the chain before learning-objectives. pipeline = [ ('needs-analysis', 'learning-objectives'), + ('course-import', 'learning-objectives'), ('learning-objectives', 'assessment-design'), ('assessment-design', 'course-builder'), ('course-builder', 'course-quality-review'), diff --git a/templates/report-format.md b/templates/report-format.md index 3cd8ea4..0b62253 100644 --- a/templates/report-format.md +++ b/templates/report-format.md @@ -6,7 +6,7 @@ Skills produce two artifacts: a JSON manifest section (system state) and an HTML - **JSON.** `.idstack/project.json` — the manifest. The skill writes its section using `bin/idstack-manifest-merge`. - **HTML.** `.idstack/exports//.html` — the report. The skill writes it using the `Write` tool, following the structure in `templates/report.html.tmpl`. -- **Pointer.** The skill's manifest section sets `report_path` to the relative path of the HTML report so other skills (and `bin/idstack-status`) can find it. +- **Pointer.** The skill's manifest section sets `report_path` to the relative path of the HTML report so other skills can find it. (`bin/idstack-status` discovers reports independently by globbing `.idstack/exports//*.html`, so the dashboard survives a stale `report_path`.) `` is derived from the manifest's `project_name` via `bin/idstack-slugify`. Empty/missing `project_name` falls back to `untitled-course`. The slug is computed deterministically; skills don't cache it. Every skill that writes a report also ensures `.idstack/exports//assets/idstack.css` exists by copying the canonical stylesheet (`$_IDSTACK/templates/assets/idstack.css`) on each run — cheap, keeps the folder self-contained when zipped or moved. diff --git a/templates/snippets/idstack-resolve.sh b/templates/snippets/idstack-resolve.sh new file mode 100644 index 0000000..b52da9e --- /dev/null +++ b/templates/snippets/idstack-resolve.sh @@ -0,0 +1,18 @@ +# Resolve the idstack install dir. Re-derived at the top of every bash block — +# blocks run in separate shells, so a value derived in an earlier block is not +# available here. Priority: explicit env overrides, Codex-style symlinks, then +# the Claude Code marketplace cache. Empty if none found; guard +# "$_IDSTACK/bin/..." calls accordingly. +_IDSTACK="" +# Marketplace cache holds one dir per installed version. Sort the basenames by +# numeric version fields, not lexically — plain sort ranks 3.9.0.0 above +# 3.10.0.0 and would pick a stale install once the minor hits double digits. +_idstack_cache_root="$HOME/.claude/plugins/cache/idstack/idstack" +_idstack_cache="" +if [ -d "$_idstack_cache_root" ]; then + _idstack_v=$(ls "$_idstack_cache_root" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1) + [ -n "$_idstack_v" ] && _idstack_cache="$_idstack_cache_root/$_idstack_v" +fi +for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$HOME/.agents/plugins/idstack" "$HOME/.agents/skills/idstack" "$_idstack_cache"; do + if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi +done diff --git a/test/integration-test.sh b/test/integration-test.sh index bf45ca7..d2309c0 100755 --- a/test/integration-test.sh +++ b/test/integration-test.sh @@ -8,15 +8,25 @@ TOTAL=0 IDSTACK_DIR="$(cd "$(dirname "$0")/.." && pwd)" TEST_DIR=$(mktemp -d) -trap "rm -rf $TEST_DIR" EXIT +# Single-quoted so expansion happens at trap time and a path with spaces survives. +trap 'rm -rf "$TEST_DIR"' EXIT + +# Snapshot the working-tree state so the suite can prove it mutated nothing — +# compared before/after rather than against a clean tree, so a developer's own +# uncommitted work doesn't trip the check. +TREE_BEFORE=$(git -C "$IDSTACK_DIR" status --porcelain 2>/dev/null || true) check() { TOTAL=$((TOTAL + 1)) - if eval "$2" 2>/dev/null; then + local _out + if _out=$(eval "$2" 2>&1); then echo " PASS: $1" PASS=$((PASS + 1)) else echo " FAIL: $1" + if [ -n "$_out" ]; then + printf '%s\n' "$_out" | head -5 | sed 's/^/ | /' + fi FAIL=$((FAIL + 1)) fi } @@ -91,8 +101,8 @@ check "no timeline shows empty state message" \ $IDSTACK_DIR/bin/idstack-timeline-log '{"skill":"needs-analysis","event":"completed","training_justified":true}' $IDSTACK_DIR/bin/idstack-timeline-log '{"skill":"course-quality-review","event":"completed","score":65,"dimensions":{"teaching_presence":7,"social_presence":3,"cognitive_presence":5}}' -check "shows skills completed checkboxes" \ - "$IDSTACK_DIR/bin/idstack-status | grep -q '\[x\] /needs-analysis'" +check "shows skills completed checkboxes (namespaced)" \ + "$IDSTACK_DIR/bin/idstack-status | grep -q '\[x\] /idstack:needs-analysis'" check "shows quality trend" \ "$IDSTACK_DIR/bin/idstack-status | grep -q 'Quality trend: 65'" @@ -100,19 +110,62 @@ check "shows quality trend" \ check "suggests next skill" \ "$IDSTACK_DIR/bin/idstack-status | grep -q 'Suggested next'" +# Quote-injection regression: a project name with an apostrophe must render, +# not blank the dashboard with a Python SyntaxError. +cat > .idstack/project.json <<'EOF' +{"version": "1.4", "project_name": "Bob's Advanced Course"} +EOF + +check "apostrophe in project name renders" \ + "'$IDSTACK_DIR/bin/idstack-status' | grep -qF \"Project: Bob's Advanced Course\"" + +check "apostrophe in project name: no 'Error reading timeline'" \ + "! '$IDSTACK_DIR/bin/idstack-status' | grep -q 'Error reading timeline'" + +# course-import is an alternative pipeline entry: with only course-import +# completed, the suggestion must be learning-objectives, not nothing. +mkdir -p importcase && ( cd importcase && \ + "$IDSTACK_DIR/bin/idstack-timeline-log" '{"skill":"course-import","event":"completed"}' ) +check "course-import alone suggests learning-objectives" \ + "( cd importcase && '$IDSTACK_DIR/bin/idstack-status' | grep -q 'Suggested next: /idstack:learning-objectives' )" + +# Everything idstack-status prints is text a user may type back. It must carry +# the /idstack: prefix for the same reason skill templates do — a bare /skill +# is not a valid command in either CLI. +check "idstack-status never prints a bare /skill command" \ + "! '$IDSTACK_DIR/bin/idstack-status' | grep -Eq '(^|[^:])/(needs-analysis|learning-objectives|assessment-design|course-builder|course-quality-review|accessibility-review|red-team|course-export|course-import|pipeline)\b'" + +rm -f .idstack/project.json + echo "" # --- idstack-gen-skills --- +# The staleness test mutates a generated SKILL.md, so it runs against a +# disposable copy of the repo under $TEST_DIR — never against the real tree +# (an interrupted run used to leave the working tree dirty). echo "## idstack-gen-skills" check "dry-run passes when fresh" \ - "$IDSTACK_DIR/bin/idstack-gen-skills --dry-run" + "'$IDSTACK_DIR/bin/idstack-gen-skills' --dry-run" + +SANDBOX="$TEST_DIR/repo" +mkdir -p "$SANDBOX" +cp -R "$IDSTACK_DIR/bin" "$IDSTACK_DIR/skills" "$IDSTACK_DIR/templates" "$IDSTACK_DIR/dist" "$SANDBOX/" +cp "$IDSTACK_DIR/AGENTS.md" "$SANDBOX/AGENTS.md" + +check "dry-run detects stale SKILL.md (sandbox)" \ + "echo 'stale content' >> '$SANDBOX/skills/needs-analysis/SKILL.md' && ! '$SANDBOX/bin/idstack-gen-skills' --dry-run" + +check "regenerate fixes staleness (sandbox)" \ + "'$SANDBOX/bin/idstack-gen-skills' && '$SANDBOX/bin/idstack-gen-skills' --dry-run" -check "dry-run detects stale SKILL.md" \ - "echo 'stale content' >> $IDSTACK_DIR/skills/needs-analysis/SKILL.md && ! $IDSTACK_DIR/bin/idstack-gen-skills --dry-run" +# A template missing {{PREAMBLE}} must be an error, not a silent SKIP that +# lets --dry-run pass green over an absent or stale output. +check "missing {{PREAMBLE}} placeholder fails dry-run and generation (sandbox)" \ + "sed -i.bak 's/{{PREAMBLE}}/PREAMBLE_GONE/' '$SANDBOX/skills/learn/SKILL.md.tmpl' && ! '$SANDBOX/bin/idstack-gen-skills' --dry-run && ! '$SANDBOX/bin/idstack-gen-skills'" -check "regenerate fixes staleness" \ - "$IDSTACK_DIR/bin/idstack-gen-skills && $IDSTACK_DIR/bin/idstack-gen-skills --dry-run" +check "real tree untouched by this suite" \ + "[ \"\$(git -C '$IDSTACK_DIR' status --porcelain 2>/dev/null || true)\" = \"\$TREE_BEFORE\" ]" echo "" echo "Results: $PASS/$TOTAL passed, $FAIL failed" diff --git a/test/mutation-test.sh b/test/mutation-test.sh new file mode 100755 index 0000000..665d217 --- /dev/null +++ b/test/mutation-test.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +# Mutation test: reintroduce each bug fixed in v3.3.0.0 into a throwaway copy +# of the repo and confirm the guarding test FAILS. A test that still passes +# with its bug reintroduced is not guarding anything — that is exactly how the +# v3.3.0.0 defects survived (the version-classifier suite tested a local copy +# of the classifier, and gen-skills counted a missing-placeholder template as +# neither generated nor failed). +# +# Slower than the gate suites (it copies the repo and runs smoke-test once per +# mutation), so it is NOT wired into smoke-test.sh. Run it after touching a +# test, a guard, or bin/lib/. CI runs it as a separate job. +# +# Usage: test/mutation-test.sh [path-to-repo] (defaults to this checkout) +set -u +SRC="${1:-$(cd "$(dirname "$0")/.." && pwd -P)}" +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT + +pass=0; fail=0; skip=0 +expect_fail() { # + local name="$1"; shift + if "$@" >/dev/null 2>&1; then + echo " NOT-GUARDED: $name (test still passed with the bug reintroduced)" + fail=$((fail+1)) + else + echo " GUARDED: $name (test failed as it should)" + pass=$((pass+1)) + fi +} + +skip_case() { # + echo " SKIPPED: $1 ($2)" + skip=$((skip+1)) +} + +# Some defects only manifest on specific interpreters. PEP 701 (Python 3.12) +# legalized reusing a quote character inside an f-string replacement field, so +# the preamble f-string bug is a SyntaxError on 3.9-3.11 and valid code on +# 3.12+. Mutating it under 3.12 would report a false NOT-GUARDED. +PY_LT_312=$(python3 -c 'import sys; print(1 if sys.version_info < (3,12) else 0)' 2>/dev/null || echo 0) + +fresh() { + rm -rf "$WORK/r" + mkdir -p "$WORK/r" + # Copy only what the suites need; skip .git and any nested worktrees. + for item in bin skills templates test evidence docs .claude-plugin \ + VERSION CHANGELOG.md README.md TODOS.md AGENTS.md setup dist; do + [ -e "$SRC/$item" ] && cp -R "$SRC/$item" "$WORK/r/" + done + return 0 +} + +# Regenerate after mutating a .tmpl or a spliced template. Without this, the +# generated files go stale and smoke-test's `gen-skills --dry-run` gate fires — +# which means the mutation is caught by the staleness check rather than by the +# assertion it is meant to exercise, and GUARDED proves nothing about that +# assertion. Regenerating makes the specific guard the only thing that can fail. +regen() { "$WORK/r/bin/idstack-gen-skills" --target all >/dev/null 2>&1 || true; } + +# 1. f-string bug in the preamble -> test-preamble-python must fail. +# Only meaningful on Python < 3.12; see PY_LT_312 above. +if [ "$PY_LT_312" = "1" ]; then + fresh + python3 - "$WORK/r/templates/preamble.md" <<'PY' +import sys +p = sys.argv[1]; s = open(p).read() +s = s.replace("print('SKILLS_COMPLETED: ' + ','.join(sorted(completed)))", + "print(f'SKILLS_COMPLETED: {','.join(sorted(completed))}')") +open(p,'w').write(s) +PY + regen + expect_fail "preamble f-string regression" "$WORK/r/test/test-preamble-python.sh" +else + skip_case "preamble f-string regression" \ + "needs python < 3.12; PEP 701 makes the mutated form valid on $(python3 -V 2>&1)" +fi + +# 2. course-import dropped from SUGGESTED_NEXT -> test-preamble-python must fail +fresh +python3 - "$WORK/r/templates/preamble.md" <<'PY' +import sys +p = sys.argv[1]; s = open(p).read() +s = s.replace(" ('course-import', 'learning-objectives'),\n", "") +open(p,'w').write(s) +PY +regen +expect_fail "course-import suggestion regression" "$WORK/r/test/test-preamble-python.sh" + +# 3. non-canonical manifest section name returns -> smoke-test must fail +fresh +sed -i.bak 's/manifest `assessments` section/manifest `assessment_design` section/' \ + "$WORK/r/skills/assessment-design/SKILL.md.tmpl" +regen +expect_fail "non-canonical section name regression" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 4. bare /skill reference returns -> smoke-test must fail +fresh +sed -i.bak 's|`/idstack:learning-objectives`|`/learning-objectives`|' \ + "$WORK/r/skills/needs-analysis/SKILL.md.tmpl" +regen +expect_fail "bare /skill reference regression" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 5. pipeline unnamespaced Skill invocation returns -> smoke-test must fail +fresh +sed -i.bak 's|skill: "idstack:needs-analysis"|skill: "needs-analysis"|' \ + "$WORK/r/skills/pipeline/SKILL.md.tmpl" +regen +expect_fail "pipeline namespace regression" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 6. hand-rolled _IDSTACK with the legacy path returns -> smoke-test must fail +fresh +python3 - "$WORK/r/skills/learn/SKILL.md.tmpl" <<'PY' +import sys +p = sys.argv[1]; s = open(p).read() +s = s.replace("{{IDSTACK_RESOLVE}}", + 'for _p in "$CLAUDE_PLUGIN_ROOT" "$HOME/.claude/plugins/idstack"; do [ -d "$_p" ] && _IDSTACK="$_p" && break; done', 1) +open(p,'w').write(s) +PY +regen +expect_fail "hand-rolled _IDSTACK regression" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 7. plugin-status fixed-window regression -> test-plugin-status must fail +fresh +python3 - "$WORK/r/bin/lib/plugin-status.sh" <<'PY' +import sys +p = sys.argv[1]; s = open(p).read() +s = s.replace('''plugin_entry_window() { + awk -v id="$1" ' + index($0, id) && !f { f = 1; print; next } + f { + if ($0 ~ /^[[:space:]]*$/ || $0 ~ /[A-Za-z0-9._-]+@[A-Za-z0-9._-]+/) { f = 0 } + else { print } + }' +}''', '''plugin_entry_window() { + grep -A4 "$1" +}''') +open(p,'w').write(s) +PY +expect_fail "plugin-status fixed-window regression" "$WORK/r/test/test-plugin-status.sh" + +# 8. version disagreement -> smoke-test must fail +fresh +printf '9.9.9.9\n' > "$WORK/r/VERSION" +expect_fail "VERSION/plugin.json disagreement" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 9. stale generated file -> smoke-test must fail (gen-skills dry-run gate) +fresh +echo 'drift' >> "$WORK/r/skills/red-team/SKILL.md" +expect_fail "stale generated SKILL.md" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 10. missing Top recommendations -> smoke-test must fail +fresh +sed -i.bak '/\*\*Top recommendations:\*\*/d' "$WORK/r/skills/learning-objectives/SKILL.md.tmpl" +regen +expect_fail "missing Top recommendations" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 11. --keep-legacy ignored by the per-skill symlink loop -> test-setup must fail +fresh +python3 - "$WORK/r/setup" <<'PY' +import sys +p = sys.argv[1]; s = open(p).read() +s = s.replace(''' if [ "$KEEP_LEGACY" = "1" ]; then + echo " KEEPING legacy symlink: $legacy (--keep-legacy)" + else + rm "$legacy" + echo " cleaned up legacy: $legacy" + fi''', ''' rm "$legacy" + echo " cleaned up legacy: $legacy"''') +open(p,'w').write(s) +PY +expect_fail "--keep-legacy ignored by per-skill loop" "$WORK/r/test/test-setup.sh" "$WORK/r" + +# 12. --local scope leaking into $HOME -> test-setup must fail +fresh +python3 - "$WORK/r/setup" <<'PY' +import sys +p = sys.argv[1]; s = open(p).read() +s = s.replace('''if [ "$SCOPE" = "local" ]; then + vestigial="$(pwd)/.claude/plugins/idstack" +else + vestigial="$HOME/.claude/plugins/idstack" +fi +if [ -L "$vestigial" ]; then + rm "$vestigial" + echo " removed vestigial symlink: $vestigial (pre-marketplace install method)" +fi''', '''for plugins_base in "$HOME/.claude/plugins" "$(pwd)/.claude/plugins"; do + vestigial="$plugins_base/idstack" + if [ -L "$vestigial" ]; then + rm "$vestigial" + echo " removed vestigial symlink: $vestigial" + fi +done''') +open(p,'w').write(s) +PY +expect_fail "--local leaking into \$HOME" "$WORK/r/test/test-setup.sh" "$WORK/r" + +# 13. silent failure when `claude` exits nonzero -> test-setup must fail +fresh +python3 - "$WORK/r/setup" <<'PY' +import sys, re +p = sys.argv[1]; s = open(p).read() +s = re.sub(r' if ! claude plugin marketplace add "\$IDSTACK_DIR"; then\n(?:.*\n)*? fi\n', + ' claude plugin marketplace add "$IDSTACK_DIR" || true\n', s, count=1) +s = re.sub(r' if ! claude plugin install idstack@idstack --scope "\$CLAUDE_SCOPE"; then\n(?:.*\n)*? fi\n', + ' claude plugin install idstack@idstack --scope "$CLAUDE_SCOPE" || true\n', s, count=1) +open(p,'w').write(s) +PY +expect_fail "silent 'claude' failure" "$WORK/r/test/test-setup.sh" "$WORK/r" + +echo "" +echo "guarded: $pass NOT guarded: $fail skipped: $skip" +[ "$fail" -eq 0 ] diff --git a/test/smoke-test.sh b/test/smoke-test.sh index 8a9b1b8..7d3dab5 100755 --- a/test/smoke-test.sh +++ b/test/smoke-test.sh @@ -10,13 +10,22 @@ TOTAL=0 # Override with $1 to point at a different checkout (CI fixtures, etc.). IDSTACK_DIR="${1:-$(cd "$(dirname "$0")/.." && pwd -P)}" +# Single source of truth for the released version — all version assertions +# derive from VERSION so a release bump can't leave this test stale. +VER="$(tr -d '[:space:]' < "$IDSTACK_DIR/VERSION" 2>/dev/null || true)" +VER3="${VER%.*}" # 4-component 3.2.0.0 -> 3-component 3.2.0 (JSON-LD softwareVersion) + check() { TOTAL=$((TOTAL + 1)) - if eval "$2" 2>/dev/null; then + local _out + if _out=$(eval "$2" 2>&1); then echo " PASS: $1" PASS=$((PASS + 1)) else echo " FAIL: $1" + if [ -n "$_out" ]; then + printf '%s\n' "$_out" | head -5 | sed 's/^/ | /' + fi FAIL=$((FAIL + 1)) fi } @@ -34,6 +43,13 @@ check "plugin manifest has name" "grep -q '\"name\": \"idstack\"' '$IDSTACK_DIR/ check "marketplace manifest exists" "[ -f '$IDSTACK_DIR/.claude-plugin/marketplace.json' ]" check "marketplace manifest names the idstack plugin" "grep -q '\"name\": \"idstack\"' '$IDSTACK_DIR/.claude-plugin/marketplace.json'" +# Version agreement — VERSION is the source of truth; plugin.json is what the +# marketplace actually serves (users only receive fixes when it bumps), and +# CHANGELOG must document every released version. +check "VERSION file exists and is non-empty" "[ -s '$IDSTACK_DIR/VERSION' ]" +check "plugin.json version matches VERSION ($VER)" "grep -qF '\"version\": \"$VER\"' '$IDSTACK_DIR/.claude-plugin/plugin.json'" +check "CHANGELOG.md has an entry for v$VER" "grep -qF '## v$VER' '$IDSTACK_DIR/CHANGELOG.md'" + # Check all skill SKILL.md files are reachable under skills/ SKILLS="needs-analysis learning-objectives course-quality-review course-import assessment-design course-builder course-export accessibility-review red-team pipeline learn" for skill in $SKILLS; do @@ -51,13 +67,20 @@ done check "evidence/references.md exists" "[ -f '$IDSTACK_DIR/evidence/references.md' ]" # Check bin scripts exist and are executable -for script in idstack-migrate idstack-timeline-log idstack-learnings-log idstack-learnings-search idstack-learnings-delete idstack-learnings-promote idstack-status idstack-gen-skills idstack-doctor idstack-slugify; do +for script in idstack-migrate idstack-timeline-log idstack-learnings-log idstack-learnings-search idstack-learnings-delete idstack-learnings-promote idstack-status idstack-gen-skills idstack-doctor idstack-slugify idstack-update-check; do check "bin/$script exists" "[ -f '$IDSTACK_DIR/bin/$script' ]" check "bin/$script is executable" "[ -x '$IDSTACK_DIR/bin/$script' ]" done +# Bash syntax gate for the shell entry points (idstack-manifest-merge is python). +for script in setup bin/idstack-doctor bin/idstack-gen-skills bin/idstack-status bin/idstack-migrate bin/idstack-slugify bin/idstack-update-check bin/lib/version-classify.sh bin/lib/plugin-status.sh; do + check "$script passes bash -n" "bash -n '$IDSTACK_DIR/$script'" +done + # Check template system check "templates/preamble.md exists" "[ -f '$IDSTACK_DIR/templates/preamble.md' ]" +check "templates/report-format.md exists" "[ -f '$IDSTACK_DIR/templates/report-format.md' ]" +check "templates/snippets/idstack-resolve.sh exists" "[ -f '$IDSTACK_DIR/templates/snippets/idstack-resolve.sh' ]" check "templates/manifest-schema.md exists" "[ -f '$IDSTACK_DIR/templates/manifest-schema.md' ]" check "templates/manifest-schema.md is non-empty" "[ -s '$IDSTACK_DIR/templates/manifest-schema.md' ]" check "templates/report.html.tmpl exists" "[ -f '$IDSTACK_DIR/templates/report.html.tmpl' ]" @@ -85,8 +108,8 @@ check "landing: indigo gradient present" "grep -q 'linear-gradient' '$LANDING'" # ~/.claude/plugins/idstack form is caught by the next check. Escaping-independent. check "landing: marketplace install command present" "grep -q 'github.com/savvides/idstack.git' '$LANDING' && grep -q 'cd idstack' '$LANDING'" check "landing: no legacy plugins-dir install string" "! grep -q '.claude/plugins/idstack' '$LANDING'" -check "landing: current version v3.2.0.0 present" "grep -q 'v3.2.0.0' '$LANDING'" -check "landing: structured-data softwareVersion 3.2.0" "grep -qF '\"softwareVersion\": \"3.2.0\"' '$LANDING'" +check "landing: current version v$VER present" "grep -qF 'v$VER' '$LANDING'" +check "landing: structured-data softwareVersion $VER3" "grep -qF '\"softwareVersion\": \"$VER3\"' '$LANDING'" check "landing: Output section present" "grep -q 'id=.output.' '$LANDING'" # Gradient-clipped text (hero h1, eyebrow) must keep a solid color fallback so it # stays visible where `background-clip: text` is unsupported. Guards against a bare @@ -120,7 +143,51 @@ for skill in $REPORT_PRODUCING_SKILLS; do check "$skill SKILL.md.tmpl references .idstack/exports/" "grep -q '\.idstack/exports/' '$IDSTACK_DIR/skills/$skill/SKILL.md.tmpl'" check "$skill SKILL.md.tmpl calls idstack-slugify" "grep -q 'idstack-slugify' '$IDSTACK_DIR/skills/$skill/SKILL.md.tmpl'" check "$skill SKILL.md.tmpl copies templates/assets/idstack.css" "grep -q 'templates/assets/idstack.css' '$IDSTACK_DIR/skills/$skill/SKILL.md.tmpl'" + # report-format.md requires a "Top recommendations" section in every report. + check "$skill SKILL.md.tmpl includes Top recommendations" "grep -qi 'Top recommendations' '$IDSTACK_DIR/skills/$skill/SKILL.md.tmpl'" +done + +# Manifest write-path contract: single-section owners go through the merge +# tool; multi-section/first-run writers document the Write-tool fallback the +# way course-import models it. +for skill in learning-objectives course-quality-review course-export; do + check "$skill SKILL.md.tmpl uses idstack-manifest-merge" "grep -q 'idstack-manifest-merge' '$IDSTACK_DIR/skills/$skill/SKILL.md.tmpl'" +done +for skill in needs-analysis course-import; do + check "$skill SKILL.md.tmpl justifies its Write-tool fallback" "grep -q 'Write-tool fallback' '$IDSTACK_DIR/skills/$skill/SKILL.md.tmpl'" +done + +# Canonical manifest section names only — these five non-canonical tokens once +# shipped in re-run checks and prose, making re-run detection dead in 5 skills. +for skill in $SKILLS; do + check "$skill SKILL.md.tmpl free of non-canonical section names" "! grep -E '\b(assessment_design|course_builder|course_export|course_import|course_quality_review)\b' '$IDSTACK_DIR/skills/$skill/SKILL.md.tmpl'" +done + +# User-facing skill references must be namespaced /idstack: — the Codex +# translation rule strips that prefix; a bare /skill renders as an invalid +# command in both CLIs. (Frontmatter descriptions are exempt; this bans the +# backticked in-body form.) +BARE_SLASH_RE='`/(needs-analysis|learning-objectives|assessment-design|course-builder|course-quality-review|accessibility-review|red-team|course-export|course-import|pipeline|learn)`' +for skill in $SKILLS; do + check "$skill SKILL.md.tmpl free of bare backticked /skill refs" "! grep -E '$BARE_SLASH_RE' '$IDSTACK_DIR/skills/$skill/SKILL.md.tmpl'" +done +check "preamble free of bare backticked /skill refs" "! grep -E '$BARE_SLASH_RE' '$IDSTACK_DIR/templates/preamble.md'" +check "no '/idstack ' space typos in templates" "! grep -rE '/idstack [a-z]' $IDSTACK_DIR/skills/*/SKILL.md.tmpl '$IDSTACK_DIR/templates/preamble.md'" +check "pipeline status table uses /idstack: prefixes" "grep -qF '[done] /idstack:needs-analysis' '$IDSTACK_DIR/skills/pipeline/SKILL.md.tmpl'" +check "pipeline invokes children with the idstack: namespace" "grep -qF 'skill: \"idstack:needs-analysis\"' '$IDSTACK_DIR/skills/pipeline/SKILL.md.tmpl'" + +# Canonical $_IDSTACK resolution: templates use the {{IDSTACK_RESOLVE}} +# placeholder, never a hand-rolled derivation (two skills once drifted to a +# path list that missed the marketplace cache — the way most users install). +RESOLVE_LINE="$(grep -m1 '^for _p in' "$IDSTACK_DIR/templates/snippets/idstack-resolve.sh" 2>/dev/null || true)" +check "resolve snippet has the canonical for-chain" "[ -n \"\$RESOLVE_LINE\" ]" +check "resolve snippet includes the marketplace cache path" "grep -q 'plugins/cache/idstack' '$IDSTACK_DIR/templates/snippets/idstack-resolve.sh'" +check "preamble embeds the snippet's resolve chain verbatim (x3)" "[ \"\$(grep -cF \"\$RESOLVE_LINE\" '$IDSTACK_DIR/templates/preamble.md')\" -eq 3 ]" +for skill in $SKILLS; do + check "$skill SKILL.md.tmpl uses {{IDSTACK_RESOLVE}}" "grep -q '{{IDSTACK_RESOLVE}}' '$IDSTACK_DIR/skills/$skill/SKILL.md.tmpl'" + check "$skill SKILL.md.tmpl does not hand-roll _IDSTACK" "! grep -Eq '_IDSTACK:?=' '$IDSTACK_DIR/skills/$skill/SKILL.md.tmpl'" done +check "no legacy .claude/plugins/idstack path in skill templates" "! grep -rF '.claude/plugins/idstack' $IDSTACK_DIR/skills/*/SKILL.md.tmpl '$IDSTACK_DIR/templates/preamble.md'" # Pipeline orchestrator must produce index.html under the course folder. # Use -E (extended regex) so the `|` alternation works under BSD grep too; @@ -174,11 +241,32 @@ fi # Version classifier (shared by setup + bin/idstack-doctor) must classify # multi-digit versions correctly. Pinned to catch the pattern-fragility -# regression Gemini flagged twice. +# regression Gemini flagged twice. The classifier itself lives in +# bin/lib/version-classify.sh — one definition sourced by setup, doctor, and +# the unit test, so the test exercises the shipped code, never a copy. +check "bin/lib/version-classify.sh exists" "[ -f '$IDSTACK_DIR/bin/lib/version-classify.sh' ]" +check "setup sources the shared version classifier" "grep -q 'lib/version-classify.sh' '$IDSTACK_DIR/setup'" +check "idstack-doctor sources the shared version classifier" "grep -q 'lib/version-classify.sh' '$IDSTACK_DIR/bin/idstack-doctor'" +check "version-classifier test sources the shared classifier" "grep -q 'lib/version-classify.sh' '$IDSTACK_DIR/test/test-version-classifier.sh'" if [ -x "$IDSTACK_DIR/test/test-version-classifier.sh" ]; then check "version-classifier unit tests pass" "'$IDSTACK_DIR/test/test-version-classifier.sh'" fi +# `claude plugin list` parsing, also extracted to bin/lib/ so it is testable. +# A fixed-size window here once reported a disabled idstack as enabled. +check "bin/lib/plugin-status.sh exists" "[ -f '$IDSTACK_DIR/bin/lib/plugin-status.sh' ]" +check "idstack-doctor sources the shared plugin-status parser" "grep -q 'lib/plugin-status.sh' '$IDSTACK_DIR/bin/idstack-doctor'" +check "idstack-doctor no longer uses a fixed -A4 window" "! grep -q 'grep -A4' '$IDSTACK_DIR/bin/idstack-doctor'" +if [ -x "$IDSTACK_DIR/test/test-plugin-status.sh" ]; then + check "plugin-status unit tests pass" "'$IDSTACK_DIR/test/test-plugin-status.sh'" +fi + +# ./setup is what a new user runs first; it is exercised against a repo copy +# with a fake $HOME and a stub `claude`, never the real install. +if [ -x "$IDSTACK_DIR/test/test-setup.sh" ]; then + check "setup behavioral tests pass" "'$IDSTACK_DIR/test/test-setup.sh' '$IDSTACK_DIR'" +fi + # Check generated files have auto-generated header for skill in $SKILLS; do check "$skill SKILL.md has auto-generated header" "grep -q 'AUTO-GENERATED from SKILL.md.tmpl' '$IDSTACK_DIR/skills/$skill/SKILL.md'" @@ -189,8 +277,9 @@ for skill in $SKILLS; do check "$skill has context recovery" "grep -q 'Context Recovery' '$IDSTACK_DIR/skills/$skill/SKILL.md'" done -# Check pipeline-originated skills have timeline logging -TIMELINE_SKILLS="needs-analysis learning-objectives course-quality-review course-import assessment-design course-builder course-export accessibility-review red-team" +# Check pipeline-originated skills have timeline logging (pipeline logs its +# own completion so "the pipeline was run" is recoverable) +TIMELINE_SKILLS="needs-analysis learning-objectives course-quality-review course-import assessment-design course-builder course-export accessibility-review red-team pipeline" for skill in $TIMELINE_SKILLS; do check "$skill has timeline logging" "grep -q 'idstack-timeline-log' '$IDSTACK_DIR/skills/$skill/SKILL.md'" done @@ -198,38 +287,51 @@ done # Check preamble uses CLAUDE_PLUGIN_ROOT check "preamble supports CLAUDE_PLUGIN_ROOT" "grep -q 'CLAUDE_PLUGIN_ROOT' '$IDSTACK_DIR/templates/preamble.md'" -# Migration tests +# The preamble's embedded python must run on the oldest supported interpreter +# (macOS system python3 is 3.9) — a SyntaxError there dies silently behind +# `2>/dev/null || true` in every generated skill. +if [ -x "$IDSTACK_DIR/test/test-preamble-python.sh" ]; then + check "preamble embedded-python tests pass" "'$IDSTACK_DIR/test/test-preamble-python.sh'" +fi + +# Migration tests. One tempdir root cleaned by trap; every cp + migrate runs +# inside a check so a migrate failure records a FAIL instead of killing the +# whole suite under `set -e` with no summary (and no leaked tempdir). FIXTURE_DIR="$IDSTACK_DIR/test/fixtures" if [ -d "$FIXTURE_DIR" ] && command -v python3 &>/dev/null; then + MIG_ROOT=$(mktemp -d) + trap 'rm -rf "$MIG_ROOT"' EXIT + # Test v1.0 → v1.4 chained migration - TMPDIR_MIG=$(mktemp -d) - cp "$FIXTURE_DIR/manifest-v1.0.json" "$TMPDIR_MIG/project.json" - "$IDSTACK_DIR/bin/idstack-migrate" "$TMPDIR_MIG/project.json" >/dev/null 2>&1 - check "v1.0→v1.4: version bumped" "python3 -c \"import json; d=json.load(open('$TMPDIR_MIG/project.json')); assert d['version']=='1.4'\"" - check "v1.0→v1.4: has preferences" "python3 -c \"import json; d=json.load(open('$TMPDIR_MIG/project.json')); assert 'preferences' in d\"" - check "v1.0→v1.4: preserves project_name" "python3 -c \"import json; d=json.load(open('$TMPDIR_MIG/project.json')); assert d['project_name']=='Test Course v1.0'\"" - rm -rf "$TMPDIR_MIG" + MIG="$MIG_ROOT/v10"; mkdir -p "$MIG" + check "v1.0→v1.4: migrate runs" "cp '$FIXTURE_DIR/manifest-v1.0.json' '$MIG/project.json' && '$IDSTACK_DIR/bin/idstack-migrate' '$MIG/project.json' >/dev/null" + check "v1.0→v1.4: version bumped" "python3 -c \"import json; d=json.load(open('$MIG/project.json')); assert d['version']=='1.4'\"" + check "v1.0→v1.4: has preferences" "python3 -c \"import json; d=json.load(open('$MIG/project.json')); assert 'preferences' in d\"" + check "v1.0→v1.4: preserves project_name" "python3 -c \"import json; d=json.load(open('$MIG/project.json')); assert d['project_name']=='Test Course v1.0'\"" + + # Test v1.1 → v1.4 migration (export_metadata.failed_items was an int count + # in v1.1; v1.2+ made it a list of item descriptors — see bin/idstack-migrate). + MIG="$MIG_ROOT/v11"; mkdir -p "$MIG" + check "v1.1→v1.4: migrate runs" "cp '$FIXTURE_DIR/manifest-v1.1.json' '$MIG/project.json' && '$IDSTACK_DIR/bin/idstack-migrate' '$MIG/project.json' >/dev/null" + check "v1.1→v1.4: version bumped" "python3 -c \"import json; d=json.load(open('$MIG/project.json')); assert d['version']=='1.4'\"" + check "v1.1→v1.4: failed_items int converted to list" "python3 -c \"import json; d=json.load(open('$MIG/project.json')); assert isinstance(d['export_metadata']['failed_items'], list)\"" # Test v1.2 → v1.4 migration - TMPDIR_MIG=$(mktemp -d) - cp "$FIXTURE_DIR/manifest-v1.2.json" "$TMPDIR_MIG/project.json" - "$IDSTACK_DIR/bin/idstack-migrate" "$TMPDIR_MIG/project.json" >/dev/null 2>&1 - check "v1.2→v1.4: version bumped" "python3 -c \"import json; d=json.load(open('$TMPDIR_MIG/project.json')); assert d['version']=='1.4'\"" - check "v1.2→v1.4: has preferences" "python3 -c \"import json; d=json.load(open('$TMPDIR_MIG/project.json')); assert d['preferences']['verbosity']=='normal'\"" - check "v1.2→v1.4: idempotent" "python3 -c \"import json; d=json.load(open('$TMPDIR_MIG/project.json')); assert d['version']=='1.4'\" && '$IDSTACK_DIR/bin/idstack-migrate' '$TMPDIR_MIG/project.json' >/dev/null 2>&1 && python3 -c \"import json; d=json.load(open('$TMPDIR_MIG/project.json')); assert d['version']=='1.4'\"" - rm -rf "$TMPDIR_MIG" + MIG="$MIG_ROOT/v12"; mkdir -p "$MIG" + check "v1.2→v1.4: migrate runs" "cp '$FIXTURE_DIR/manifest-v1.2.json' '$MIG/project.json' && '$IDSTACK_DIR/bin/idstack-migrate' '$MIG/project.json' >/dev/null" + check "v1.2→v1.4: version bumped" "python3 -c \"import json; d=json.load(open('$MIG/project.json')); assert d['version']=='1.4'\"" + check "v1.2→v1.4: has preferences" "python3 -c \"import json; d=json.load(open('$MIG/project.json')); assert d['preferences']['verbosity']=='normal'\"" + check "v1.2→v1.4: idempotent" "'$IDSTACK_DIR/bin/idstack-migrate' '$MIG/project.json' >/dev/null && python3 -c \"import json; d=json.load(open('$MIG/project.json')); assert d['version']=='1.4'\"" # Test v1.3-drifted → v1.4 cleanup migration (renames red_team_audit.summary.*_count # to red_team_audit.findings_summary.*, moves _import_quality_flags into # import_metadata.quality_flag_details). if [ -f "$FIXTURE_DIR/manifest-v1.3-drifted.json" ]; then - TMPDIR_MIG=$(mktemp -d) - cp "$FIXTURE_DIR/manifest-v1.3-drifted.json" "$TMPDIR_MIG/project.json" - "$IDSTACK_DIR/bin/idstack-migrate" "$TMPDIR_MIG/project.json" >/dev/null 2>&1 - check "v1.3-drifted→v1.4: version bumped" "python3 -c \"import json; d=json.load(open('$TMPDIR_MIG/project.json')); assert d['version']=='1.4'\"" - check "v1.3-drifted→v1.4: red_team summary renamed to findings_summary" "python3 -c \"import json; d=json.load(open('$TMPDIR_MIG/project.json')); rt=d['red_team_audit']; assert 'summary' not in rt; assert rt['findings_summary']=={'critical': 3, 'warning': 5, 'info': 2}\"" - check "v1.3-drifted→v1.4: _import_quality_flags moved into import_metadata" "python3 -c \"import json; d=json.load(open('$TMPDIR_MIG/project.json')); assert '_import_quality_flags' not in d; details=d['import_metadata']['quality_flag_details']; assert len(details)==2 and details[0]['key']=='orphan_module_8'\"" - rm -rf "$TMPDIR_MIG" + MIG="$MIG_ROOT/v13"; mkdir -p "$MIG" + check "v1.3-drifted→v1.4: migrate runs" "cp '$FIXTURE_DIR/manifest-v1.3-drifted.json' '$MIG/project.json' && '$IDSTACK_DIR/bin/idstack-migrate' '$MIG/project.json' >/dev/null" + check "v1.3-drifted→v1.4: version bumped" "python3 -c \"import json; d=json.load(open('$MIG/project.json')); assert d['version']=='1.4'\"" + check "v1.3-drifted→v1.4: red_team summary renamed to findings_summary" "python3 -c \"import json; d=json.load(open('$MIG/project.json')); rt=d['red_team_audit']; assert 'summary' not in rt; assert rt['findings_summary']=={'critical': 3, 'warning': 5, 'info': 2}\"" + check "v1.3-drifted→v1.4: _import_quality_flags moved into import_metadata" "python3 -c \"import json; d=json.load(open('$MIG/project.json')); assert '_import_quality_flags' not in d; details=d['import_metadata']['quality_flag_details']; assert len(details)==2 and details[0]['key']=='orphan_module_8'\"" fi fi diff --git a/test/test-manifest-merge.sh b/test/test-manifest-merge.sh index 51206c0..a3ab89a 100755 --- a/test/test-manifest-merge.sh +++ b/test/test-manifest-merge.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# (see idstack-migrate --init coverage at the end of this file) # Unit tests for bin/idstack-manifest-merge. # Run from the repo root (or sourced by smoke-test.sh). @@ -135,6 +136,47 @@ EC=$? set -e assert "manifest with string root exits 2" "[ $EC -eq 2 ]" +# --- idstack-migrate --init: the manifest bootstrap the merge tool depends on --- +# Skills running standalone create the manifest with --init before merging; +# without it the merge exits 4 and standalone results are never persisted. +MIGRATE="$(cd "$(dirname "$0")/.." && pwd -P)/bin/idstack-migrate" + +INIT_DIR="$WORK/initcase" +mkdir -p "$INIT_DIR/.idstack" +( cd "$INIT_DIR" && "$MIGRATE" --init .idstack/project.json >/dev/null 2>&1 ) +assert "--init creates a manifest" "[ -f '$INIT_DIR/.idstack/project.json' ]" +assert "--init manifest is at the latest schema version" \ + "python3 -c \"import json; assert json.load(open('$INIT_DIR/.idstack/project.json'))['version']=='1.4'\"" + +# Every section the merge tool accepts must exist in a freshly initialized +# manifest — otherwise a standalone skill's first merge lands in a document +# missing the sections downstream skills read. +assert "--init manifest carries every mergeable section" \ + "python3 -c \" +import json, re, sys +d = json.load(open('$INIT_DIR/.idstack/project.json')) +src = open('$MERGE').read() +allowed = set(re.findall(r'\\\"([a-z_]+)\\\",?\\s*(?:#|\\n)', src.split('ALLOWED_SECTIONS')[1].split(']')[0])) +missing = sorted(s for s in allowed if s not in d) +assert not missing, 'missing from --init manifest: ' + repr(missing) +\"" + +# A merge into a freshly initialized manifest must succeed. +( cd "$INIT_DIR" && echo '{"overall_score": 77}' | "$MERGE" --section quality_review --payload - --quiet >/dev/null 2>&1 ) +assert "merge into an --init manifest succeeds" \ + "python3 -c \"import json; assert json.load(open('$INIT_DIR/.idstack/project.json'))['quality_review']['overall_score']==77\"" + +# --init must never clobber an existing manifest. +( cd "$INIT_DIR" && "$MIGRATE" --init .idstack/project.json >/dev/null 2>&1 ) +assert "--init preserves an existing manifest" \ + "python3 -c \"import json; assert json.load(open('$INIT_DIR/.idstack/project.json'))['quality_review']['overall_score']==77\"" + +# Without --init the old behavior holds: silent no-op on a missing file. +NOINIT_DIR="$WORK/noinit" +mkdir -p "$NOINIT_DIR/.idstack" +( cd "$NOINIT_DIR" && "$MIGRATE" .idstack/project.json >/dev/null 2>&1 ) +assert "plain migrate does not create a manifest" "[ ! -f '$NOINIT_DIR/.idstack/project.json' ]" + echo "" echo "manifest-merge: $PASS/$TOTAL passed, $FAIL failed" [ "$FAIL" -eq 0 ] && exit 0 || exit 1 diff --git a/test/test-plugin-status.sh b/test/test-plugin-status.sh new file mode 100755 index 0000000..8812b47 --- /dev/null +++ b/test/test-plugin-status.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# Unit tests for bin/lib/plugin-status.sh — the `claude plugin list` parsing +# that bin/idstack-doctor uses to decide whether idstack is enabled. +# +# Why this test exists: the original implementation used `grep -A4`, a fixed +# window that spills into the NEXT plugin's entry. With two plugins listed +# close together, a disabled idstack was reported as "installed and enabled" +# and the doctor gave a clean bill of health for a broken install. The cases +# below pin the scoping rule: an entry ends at a blank line or the next +# name@marketplace id, whichever comes first. +# +# Run from the repo root or via smoke-test.sh. +set -e + +PASS=0 +FAIL=0 +TOTAL=0 + +IDSTACK_DIR="$(cd "$(dirname "$0")/.." && pwd -P)" +. "$IDSTACK_DIR/bin/lib/plugin-status.sh" + +# check +check() { + TOTAL=$((TOTAL + 1)) + local name="$1" expected="$2" listing="$3" got + if printf '%s\n' "$listing" | plugin_is_enabled "idstack@idstack"; then + got="enabled" + else + got="disabled" + fi + if [ "$got" = "$expected" ]; then + PASS=$((PASS + 1)) + echo " PASS: $name -> $got" + else + FAIL=$((FAIL + 1)) + echo " FAIL: $name -> $got (expected $expected)" + fi +} + +echo "test-plugin-status" +echo "" + +check "enabled, only plugin installed" enabled \ +'idstack@idstack + Status: enabled + Version: 3.3.0.0' + +check "disabled, only plugin installed" disabled \ +'idstack@idstack + Status: disabled + Version: 3.3.0.0' + +# The regression the fixed window caused: the neighbour's "enabled" was inside +# the -A4 window, so a disabled idstack read as enabled. +check "disabled, next plugin enabled two lines later" disabled \ +'idstack@idstack + Status: disabled +superpowers@marketplace + Status: enabled' + +check "disabled, next plugin enabled after a blank line" disabled \ +'idstack@idstack + Status: disabled + Version: 3.3.0.0 + +superpowers@marketplace + Status: enabled' + +check "disabled, a PRECEDING plugin is enabled" disabled \ +'superpowers@marketplace + Status: enabled + +idstack@idstack + Status: disabled' + +check "enabled, listed between two disabled plugins" enabled \ +'airtable@marketplace + Status: disabled + +idstack@idstack + Status: enabled + +superpowers@marketplace + Status: disabled' + +# Single-line listing formats. +check "single-line format, enabled" enabled 'idstack@idstack (enabled)' +check "single-line format, disabled with enabled neighbour" disabled \ +'idstack@idstack (disabled) +superpowers@marketplace (enabled)' + +# Not installed at all — doctor gates on a separate grep, but the parser must +# not invent an enabled verdict from someone else's entry. +check "idstack absent, another plugin enabled" disabled \ +'superpowers@marketplace + Status: enabled' + +echo "" +echo " $PASS/$TOTAL passed" +[ "$FAIL" = "0" ] || exit 1 diff --git a/test/test-preamble-python.sh b/test/test-preamble-python.sh new file mode 100755 index 0000000..77f24a1 --- /dev/null +++ b/test/test-preamble-python.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# Executes the Context Recovery bash block from templates/preamble.md against a +# fixture timeline, on THIS machine's python3. The block swallows errors with +# `2>/dev/null || true`, so an embedded-Python SyntaxError surfaces as empty +# output rather than a crash — the positive assertions below are what catch it. +# (A same-type-quote f-string once broke the whole block on every Python < 3.12 +# and no test noticed; this suite exists so that class of failure can't ship.) +# +# Run from the repo root or via smoke-test.sh; CI runs it on Python 3.9 and 3.12. +set -e + +PASS=0 +FAIL=0 +TOTAL=0 + +IDSTACK_DIR="$(cd "$(dirname "$0")/.." && pwd -P)" + +check() { + TOTAL=$((TOTAL + 1)) + local _out + if _out=$(eval "$2" 2>&1); then + echo " PASS: $1" + PASS=$((PASS + 1)) + else + echo " FAIL: $1" + if [ -n "$_out" ]; then + printf '%s\n' "$_out" | head -5 | sed 's/^/ | /' + fi + FAIL=$((FAIL + 1)) + fi +} + +echo "test-preamble-python" +echo " python3: $(python3 --version 2>&1 || echo 'not found')" +echo "" + +if ! command -v python3 >/dev/null 2>&1; then + echo "SKIP: python3 not available — the preamble degrades without it by design." + exit 0 +fi + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT + +# Extract the Context Recovery bash fence from the preamble source. Generated +# SKILL.md files embed this block byte-for-byte (gen-skills splices it), so +# testing the source covers all 22 outputs. +awk '/^## Preamble: Context Recovery/{s=1} s && /^```bash$/{f=1; next} f && /^```$/{exit} f' \ + "$IDSTACK_DIR/templates/preamble.md" > "$WORK/block.sh" + +check "extracted a non-empty context-recovery block" "[ -s '$WORK/block.sh' ]" +check "block reads the timeline" "grep -q 'timeline.jsonl' '$WORK/block.sh'" + +mkdir -p "$WORK/proj/.idstack" +cat > "$WORK/proj/.idstack/timeline.jsonl" <<'EOF' +{"ts":"2026-08-04T00:00:00Z","skill":"needs-analysis","event":"completed"} +{"ts":"2026-08-04T00:01:00Z","skill":"course-import","event":"completed"} +EOF + +OUT=$(cd "$WORK/proj" && bash "$WORK/block.sh" 2>&1 || true) + +check "SKILLS_COMPLETED lists both skills" \ + "echo \"\$OUT\" | grep -qF 'SKILLS_COMPLETED: course-import,needs-analysis'" +check "LAST_SKILL reported" \ + "echo \"\$OUT\" | grep -q 'LAST_SKILL: course-import'" +check "SUGGESTED_NEXT after needs-analysis is learning-objectives" \ + "echo \"\$OUT\" | grep -qF 'SUGGESTED_NEXT: learning-objectives'" + +# Import-first project: course-import is the alternative pipeline entry, and it +# alone must still yield a suggestion. +mkdir -p "$WORK/proj2/.idstack" +cat > "$WORK/proj2/.idstack/timeline.jsonl" <<'EOF' +{"ts":"2026-08-04T00:01:00Z","skill":"course-import","event":"completed"} +EOF + +OUT2=$(cd "$WORK/proj2" && bash "$WORK/block.sh" 2>&1 || true) + +check "import-first project gets SUGGESTED_NEXT: learning-objectives" \ + "echo \"\$OUT2\" | grep -qF 'SUGGESTED_NEXT: learning-objectives'" + +echo "" +echo "Results: $PASS/$TOTAL passed, $FAIL failed" +[ "$FAIL" -eq 0 ] && exit 0 || exit 1 diff --git a/test/test-setup.sh b/test/test-setup.sh new file mode 100755 index 0000000..fd7e3fc --- /dev/null +++ b/test/test-setup.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +# Behavioral tests for ./setup — flag parsing, scope selection, and legacy +# cleanup. Everything runs against a COPY of the repo with a fake $HOME and a +# stub `claude` on $PATH, so the real install and the real working tree are +# never touched. +# +# Why this exists: setup is the primary deliverable (it is what a new user +# runs first) and had no test coverage at all, while the smoke test spent 14 +# assertions on landing-page CSS. The v3.3.0.0 audit found four defects in it, +# including --keep-legacy being honored in only one of three deletion paths. +# +# Usage: test/test-setup.sh [path-to-repo] +set -u + +PASS=0 +FAIL=0 +TOTAL=0 + +SRC="${1:-$(cd "$(dirname "$0")/.." && pwd -P)}" +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT + +check() { + TOTAL=$((TOTAL + 1)) + local name="$1" cmd="$2" _out + if _out=$(eval "$cmd" 2>&1); then + echo " PASS: $name" + PASS=$((PASS + 1)) + else + echo " FAIL: $name" + [ -n "$_out" ] && printf '%s\n' "$_out" | head -5 | sed 's/^/ | /' + FAIL=$((FAIL + 1)) + fi +} + +# Build an isolated environment: repo copy + fake HOME + stub claude/codex that +# log their arguments instead of touching the real install. +setup_env() { + rm -rf "$WORK/env" + mkdir -p "$WORK/env/home" "$WORK/env/bin" "$WORK/env/idstack" + for item in bin skills templates evidence docs .claude-plugin \ + VERSION setup dist AGENTS.md; do + [ -e "$SRC/$item" ] && cp -R "$SRC/$item" "$WORK/env/idstack/" + done + cat > "$WORK/env/bin/claude" <<'EOF' +#!/usr/bin/env bash +echo "claude $*" >> "$CLAUDE_STUB_LOG" +exit "${CLAUDE_STUB_EXIT:-0}" +EOF + chmod +x "$WORK/env/bin/claude" + : > "$WORK/env/claude.log" + return 0 +} + +# run_setup — runs setup with the fake HOME and stub PATH. +run_setup() { + ( export HOME="$WORK/env/home" \ + PATH="$WORK/env/bin:$PATH" \ + CLAUDE_STUB_LOG="$WORK/env/claude.log" \ + CLAUDE_STUB_EXIT="${STUB_EXIT:-0}" + cd "$WORK/env/idstack" && ./setup --no-codex "$@" ) >"$WORK/env/out.log" 2>&1 +} + +echo "test-setup" +echo "" + +# raw_setup — like run_setup but returns setup's exit status and +# leaves stdout on the caller's stdout (for flag-parsing assertions). +raw_setup() { + ( export HOME="$WORK/env/home" \ + PATH="$WORK/env/bin:$PATH" \ + CLAUDE_STUB_LOG="$WORK/env/claude.log" \ + CLAUDE_STUB_EXIT="${STUB_EXIT:-0}" + cd "$WORK/env/idstack" && ./setup "$@" ) +} + +# --- flag parsing --- +setup_env +check "unknown flag exits 2" \ + "raw_setup --bogus >/dev/null 2>&1; [ \$? -eq 2 ]" + +setup_env +check "--help exits 0 and documents --keep-legacy" \ + "raw_setup --help 2>/dev/null | grep -q -- '--keep-legacy'" + +# --- scope selection --- +setup_env +run_setup +check "default install uses user scope" \ + "grep -q 'plugin install idstack@idstack --scope user' '$WORK/env/claude.log'" + +setup_env +run_setup --local +check "--local installs at project scope" \ + "grep -q 'plugin install idstack@idstack --scope project' '$WORK/env/claude.log'" + +# --- legacy cleanup: v2.0 dispatcher symlink --- +setup_env +mkdir -p "$WORK/env/home/.claude/skills" +ln -s "$WORK/env/idstack" "$WORK/env/home/.claude/skills/idstack" +run_setup +check "legacy dispatcher symlink is removed by default" \ + "[ ! -e '$WORK/env/home/.claude/skills/idstack' ]" + +setup_env +mkdir -p "$WORK/env/home/.claude/skills" +ln -s "$WORK/env/idstack" "$WORK/env/home/.claude/skills/idstack" +run_setup --keep-legacy +check "--keep-legacy KEEPS the dispatcher symlink" \ + "[ -L '$WORK/env/home/.claude/skills/idstack' ]" + +# --- legacy cleanup: pre-v2 per-skill symlinks --- +setup_env +mkdir -p "$WORK/env/home/.claude/skills" +ln -s "$WORK/env/idstack/skills/needs-analysis" "$WORK/env/home/.claude/skills/needs-analysis" +run_setup +check "pre-v2 per-skill symlink is removed by default" \ + "[ ! -e '$WORK/env/home/.claude/skills/needs-analysis' ]" + +setup_env +mkdir -p "$WORK/env/home/.claude/skills" +ln -s "$WORK/env/idstack/skills/needs-analysis" "$WORK/env/home/.claude/skills/needs-analysis" +run_setup --keep-legacy +check "--keep-legacy KEEPS pre-v2 per-skill symlinks" \ + "[ -L '$WORK/env/home/.claude/skills/needs-analysis' ]" + +# --- legacy cleanup: pre-v2.0.1.0 dispatcher DIRECTORY --- +setup_env +mkdir -p "$WORK/env/home/.claude/skills/idstack" +printf 'name: idstack\n' > "$WORK/env/home/.claude/skills/idstack/SKILL.md" +run_setup +check "pre-v2.0.1.0 dispatcher dir is removed by default" \ + "[ ! -d '$WORK/env/home/.claude/skills/idstack' ]" + +setup_env +mkdir -p "$WORK/env/home/.claude/skills/idstack" +printf 'name: idstack\n' > "$WORK/env/home/.claude/skills/idstack/SKILL.md" +run_setup --keep-legacy +check "--keep-legacy KEEPS the dispatcher dir" \ + "[ -d '$WORK/env/home/.claude/skills/idstack' ]" + +# An unrecognized directory is never auto-removed. +setup_env +mkdir -p "$WORK/env/home/.claude/skills/idstack" +printf 'someone else stuff\n' > "$WORK/env/home/.claude/skills/idstack/README.md" +run_setup +check "unrecognized dir at the legacy path is left alone" \ + "[ -d '$WORK/env/home/.claude/skills/idstack' ]" + +# --- vestigial symlink scope: --local must not touch \$HOME --- +setup_env +mkdir -p "$WORK/env/home/.claude/plugins" +ln -s "$WORK/env/idstack" "$WORK/env/home/.claude/plugins/idstack" +run_setup --local +check "--local leaves the \$HOME vestigial symlink alone" \ + "[ -L '$WORK/env/home/.claude/plugins/idstack' ]" + +setup_env +mkdir -p "$WORK/env/home/.claude/plugins" +ln -s "$WORK/env/idstack" "$WORK/env/home/.claude/plugins/idstack" +run_setup +check "default scope removes the \$HOME vestigial symlink" \ + "[ ! -e '$WORK/env/home/.claude/plugins/idstack' ]" + +# --- failure handling: a nonzero `claude` must fail loudly, not silently --- +setup_env +check "a failing 'claude' call makes setup exit nonzero" \ + "! STUB_EXIT=1 raw_setup --no-codex >/dev/null 2>&1" + +setup_env +STUB_EXIT=1 run_setup +check "a failing 'claude' call prints manual-recovery steps" \ + "grep -q 'marketplace add' '$WORK/env/out.log'" + +# --- regeneration invariant --- +# The fixture is copied from a tree the smoke test already gates as fresh, so +# asserting freshness after a plain run proves nothing. Dirty a generated file +# first: only an actual regeneration during setup can make the dry-run pass. +setup_env +echo 'drift' >> "$WORK/env/idstack/skills/red-team/SKILL.md" +check "fixture is genuinely stale before setup runs" \ + "! '$WORK/env/idstack/bin/idstack-gen-skills' --dry-run" +run_setup +check "setup regenerates stale generated files" \ + "'$WORK/env/idstack/bin/idstack-gen-skills' --dry-run" + +echo "" +echo "Results: $PASS/$TOTAL passed, $FAIL failed" +[ "$FAIL" -eq 0 ] && exit 0 || exit 1 diff --git a/test/test-version-classifier.sh b/test/test-version-classifier.sh index 312d15f..ea4c45b 100755 --- a/test/test-version-classifier.sh +++ b/test/test-version-classifier.sh @@ -1,9 +1,8 @@ #!/usr/bin/env bash # Unit tests for the legacy-VERSION classifier shared by setup and -# bin/idstack-doctor. Both files use the same two-arm case statement to -# decide whether a VERSION file under the legacy install path means -# "modern install — leave alone" (skip) or "pre-v2.0.1.0 install — flag -# for cleanup" (legacy). +# bin/idstack-doctor. The classifier decides whether a VERSION file under the +# legacy install path means "modern install — leave alone" (skip) or +# "pre-v2.0.1.0 install — flag for cleanup" (legacy). # # Why this test exists: Gemini Code Assist has flagged this case statement # three times (PR #15 → PR #19 → PR #20 → PR #21). The patterns are subtle @@ -12,6 +11,11 @@ # contract so it can't drift again. Cases that once silently fell through # to "unknown" — notably 20.x and 200.x — are now pinned here explicitly. # +# The single definition lives in bin/lib/version-classify.sh and is sourced +# here, so this suite exercises the exact code setup and doctor run — a local +# mirror of the case statement once passed green while the real call sites +# could drift. +# # Run from the repo root or via smoke-test.sh. set -e @@ -20,15 +24,8 @@ PASS=0 FAIL=0 TOTAL=0 -# Mirror of the case statement in setup and bin/idstack-doctor. Keep these -# patterns in lockstep with both files — if you change one, change all three. -classify_version() { - case "$1" in - 2.0.[1-9]*|2.[1-9]*|[3-9]*|[1-9][0-9]*) echo "skip" ;; - 0.*|1.*|2.0.0.*|2.0.0) echo "legacy" ;; - *) echo "unknown" ;; - esac -} +IDSTACK_DIR="$(cd "$(dirname "$0")/.." && pwd -P)" +. "$IDSTACK_DIR/bin/lib/version-classify.sh" check() { TOTAL=$((TOTAL + 1))