From 817cf3d76120d15dbbb29f5be73004408cfd31d0 Mon Sep 17 00:00:00 2001 From: Philippos Savvides Date: Sun, 4 Oct 2026 16:50:28 -0700 Subject: [PATCH 1/7] docs: plan the post-STE follow-up fixes Four fixes found while running PR #113's manual test plan: the flaky rendered landing test, the resolve chain that never finds the loaded plugin, the untitled-course report folder, and T1 citations of T5 sources. The plan was dry-run end to end on a scratch copy. Co-Authored-By: Claude Opus 5.5 --- .../2026-10-04-post-ste-followup-fixes.md | 1127 +++++++++++++++++ 1 file changed, 1127 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-04-post-ste-followup-fixes.md diff --git a/docs/superpowers/plans/2026-10-04-post-ste-followup-fixes.md b/docs/superpowers/plans/2026-10-04-post-ste-followup-fixes.md new file mode 100644 index 0000000..13590f5 --- /dev/null +++ b/docs/superpowers/plans/2026-10-04-post-ste-followup-fixes.md @@ -0,0 +1,1127 @@ +# Post-STE Follow-up Fixes Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix four defects found while running PR #113's manual test plan: +1. A flaky Chrome startup in the rendered landing test. +2. A resolve chain that never finds the loaded plugin, so skills run stale cached binaries and skip the writing check silently. +3. A report folder named `untitled-course` on a first run. +4. Evidence citations that claim T1 for T5 sources. + +**Architecture:** Each defect is one task with its own tests and its own commit, in one PR. Tests come first. Each task adds a regression check to `test/smoke-test.sh` or a new checker that smoke runs. Each task also adds a mutation case to `test/mutation-test.sh` that puts the defect back and must fail. Skill text changes happen in `skills/*/SKILL.md.tmpl` and shared templates. `bin/idstack-gen-skills` then regenerates the `SKILL.md` files. + +**Tech Stack:** Bash (3.2-compatible), Python 3.9+ standard library, Node 22 (CDP over the global `WebSocket`), headless Chrome. + +## Global Constraints + +- Python in any new or changed file must run on Python 3.9 (`/usr/bin/python3` on macOS). No modern-only syntax. +- Bash in skill templates must run in macOS `/bin/bash` 3.2. +- All text that idstack shows a person must follow ASD-STE100. This includes skill text, preamble rules, CLI messages and new test failure messages. Run `bin/idstack-ste-check` (with `--format markdown` for prose) on new wording. Do not use semicolons, "consider", "may", "should" or "suggest". +- Edit by anchor, never by line number. Three tasks edit `skills/needs-analysis/SKILL.md.tmpl` and two edit `skills/course-import/SKILL.md.tmpl`, so line numbers drift. Every scripted edit asserts that its anchor occurs exactly as often as expected. +- Never edit a generated `skills/*/SKILL.md` by hand. After any `.tmpl`, `templates/preamble.md`, `templates/manifest-schema.md` or `templates/snippets/` change, run `bin/idstack-gen-skills`. +- Mutation case numbers are 50 (Task 1), 51a–51c (Task 2), 52a–52b (Task 3) and 53a–53c (Task 4). Insert every new case directly before the footer of `test/mutation-test.sh`. The footer is the unique pair of lines `echo ""` and `echo "guarded: $pass NOT guarded: $fail skipped: $skip"`. +- Do not change `VERSION`, `.claude-plugin/plugin.json`, `CHANGELOG.md` or the extension version. No release is needed (see Task 6). +- Do not record smoke-test totals in any task. Task 5 measures the real total once and writes it to `CLAUDE.md`. Local runs inside a scratch copy of the repo skip 12 legacy-install checks. So always run smoke from this worktree with no argument. +- This session's sandbox refuses a Bash command that combines the repo path (it contains "github") with compound shell syntax, saying it "names git in a form too complex". If that happens, save the commands in a `.sh` file under the scratchpad and run `bash `. Run git commands as plain, separate commands. +- This plan was dry-run end to end. Every step was applied in order to a scratch copy of the repo. Smoke passed with 0 failed, and every scripted edit's anchor counts held. If an anchor assert fails, stop and report it. Do not loosen the assert. +- Work on branch `fix/post-ste-followups` in worktree `/Users/philippossavvides/github/idstack/.claude/worktrees/feat-ste-output`. Commit message bodies end with `Co-Authored-By: Claude Opus 5.5 `. + +## File Map + +| File | Task | Responsibility | +|---|---|---| +| `test/test-rendered-landing.js` | 1 | Launch Chrome on port 0. Read the DevTools address from stderr, and fail fast when Chrome exits. | +| `templates/snippets/idstack-resolve.sh`, `templates/preamble.md`, `templates/manifest-schema.md` | 2 | Resolve chain uses the exact `${CLAUDE_PLUGIN_ROOT}` token. Add preamble rules for the two check tokens. | +| 14 check-step lines in 11 `skills/*/SKILL.md.tmpl` files, plus prose in `learn` and `course-export` | 2 | A distinct `STE_CHECK_MISSING: ` token. | +| `skills/needs-analysis/SKILL.md.tmpl`, `skills/course-import/SKILL.md.tmpl` | 3 | Report slug comes from the session's course title. | +| `test/check-citation-tiers.py` (new) | 4 | Compares every `[Code-N] [Tn]` citation with `evidence/references.md`. | +| 4 skill templates (needs-analysis, course-builder, course-quality-review, learning-objectives) | 4 | Correct 16 citations. | +| `test/smoke-test.sh`, `test/mutation-test.sh` | 1–4 | Regression checks and mutation guards. | +| `CLAUDE.md`, `CONTRIBUTING.md` | 2, 4, 5 | Resolve-order note, new checker, smoke total. | + +--- + +### Task 1: Rendered landing test survives a slow Chrome start and explains a failed one + +**Why:** On a cold ubuntu runner the test failed twice with `Chrome did not expose a debugging port within 12s`. Each time it passed on a rerun of the same commit. These problems caused it: +- A fixed port (`9400 + pid % 500`). +- A 12-second probe loop with no timeout on each `fetch`. +- Chrome's stderr discarded and its exit never watched. + +The failures left no clue. + +The fix: +- `--remote-debugging-port=0`, with the `DevTools listening on ws://…` stderr line as the address. +- A 30-second deadline. +- Immediate failure, with the exit code and the last stderr lines, when Chrome exits early. + +No retry: port 0 removes clashes, and the longer deadline covers a slow start. + +**Files:** +- Modify: `test/test-rendered-landing.js` (the `port` constant and the spawn and probe block in `main()`) +- Modify: `test/smoke-test.sh` (after the `rendered landing page tests pass` check and in the node-absent SKIP block) +- Modify: `test/mutation-test.sh` (new case 50) + +**Interfaces:** +- Consumes: `check [want_exit] [want_output_regex]` from `test/test-helper.sh`. +- Produces: the error texts `Chrome stopped (exit code N) before it opened a debugging port. Last output: …` and `Chrome did not open a debugging port within 30s. Last output: …`. Mutation 50 anchors on the line ` chrome.on('exit', (code, signal) => {`. + +- [ ] **Step 1: Write the failing regression check** + +In `test/smoke-test.sh`, replace this anchor (it occurs once): + +````bash + check "rendered landing page tests pass" "node '$IDSTACK_DIR/test/test-rendered-landing.js'" +```` + +with: + +````bash + check "rendered landing page tests pass" "node '$IDSTACK_DIR/test/test-rendered-landing.js'" + # A Chrome that stops at startup must fail the test at once and give its exit code and last + # output. Before, the test waited 12s and said only that no debugging port opened, so two CI + # failures left no clue. Needs no real browser: CHROME_PATH points at a stub that stops. + FAKE_CHROME_DIR=$(mktemp -d) + printf '#!/bin/sh\necho "fake-chrome-startup-failure" >&2\nexit 3\n' > "$FAKE_CHROME_DIR/chrome" + chmod +x "$FAKE_CHROME_DIR/chrome" + check "rendered landing test reports why Chrome stopped at startup" \ + "CHROME_PATH='$FAKE_CHROME_DIR/chrome' node '$IDSTACK_DIR/test/test-rendered-landing.js'" \ + 1 "exit code 3.*fake-chrome-startup-failure" + rm -rf "$FAKE_CHROME_DIR" +```` + +Also replace the anchor `echo " SKIP: rendered landing page tests (node not installed)"` with: + +````bash + echo " SKIP: rendered landing page tests (node not installed)" + echo " SKIP: rendered landing startup report (node not installed)" +```` + +- [ ] **Step 2: Run it and see it fail** + +Run: `./test/smoke-test.sh 2>&1 | grep -E "FAIL|Results"` +Expected: exactly one FAIL, `rendered landing test reports why Chrome stopped at startup (exit 1 correct, output missing /exit code 3.*fake-chrome-startup-failure/)`. It comes after a wait of about 13 s. + +- [ ] **Step 3: Replace the fixed port with a startup deadline** + +In `test/test-rendered-landing.js`, replace: + +````js +const userDataDir = path.join(os.tmpdir(), `idstack-rendered-${process.pid}`); +const port = 9400 + (process.pid % 500); +let chrome = null; +```` + +with: + +````js +const userDataDir = path.join(os.tmpdir(), `idstack-rendered-${process.pid}`); +// A cold Chrome on a fresh ubuntu runner has missed a 12s deadline. The wait costs time only +// when Chrome is stuck, so it is generous. +const STARTUP_SECONDS = 30; +let chrome = null; +```` + +- [ ] **Step 4: Replace the spawn and the probe loop** + +In `test/test-rendered-landing.js`, replace: + +````js + chrome = spawn(chromePath, [ + '--headless=new', `--remote-debugging-port=${port}`, + '--no-first-run', '--no-default-browser-check', '--disable-gpu', + '--hide-scrollbars', '--no-sandbox', + `--user-data-dir=${userDataDir}`, 'about:blank', + ], { stdio: 'ignore' }); + + let wsUrl = null; + for (let i = 0; i < 80; i++) { + try { + const res = await fetch(`http://127.0.0.1:${port}/json/version`); + wsUrl = (await res.json()).webSocketDebuggerUrl; + break; + } catch { await sleep(150); } + } + if (!wsUrl) throw new Error('Chrome did not expose a debugging port within 12s'); +```` + +with: + +````js + // Port 0: Chrome picks a free port and prints its address on stderr, so no other process can + // hold the port. Keep reading stderr for the whole run. A full pipe stops Chrome. + chrome = spawn(chromePath, [ + '--headless=new', '--remote-debugging-port=0', + '--no-first-run', '--no-default-browser-check', '--disable-gpu', + '--hide-scrollbars', '--no-sandbox', + `--user-data-dir=${userDataDir}`, 'about:blank', + ], { stdio: ['ignore', 'ignore', 'pipe'] }); + + let stderr = ''; + const lastOutput = () => stderr.trim().split('\n').slice(-3).join(' / '); + const wsUrl = await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error( + `Chrome did not open a debugging port within ${STARTUP_SECONDS}s. Last output: ${lastOutput()}`)), + STARTUP_SECONDS * 1000); + chrome.stderr.on('data', chunk => { + stderr += chunk; + const m = stderr.match(/DevTools listening on (ws:\/\/\S+)/); + if (m) { clearTimeout(timer); resolve(m[1]); } + stderr = stderr.slice(-2000); + }); + chrome.on('error', err => { clearTimeout(timer); reject(err); }); + chrome.on('exit', (code, signal) => { + clearTimeout(timer); + reject(new Error(`Chrome stopped (${signal || `exit code ${code}`}) before it opened ` + + `a debugging port. Last output: ${lastOutput()}`)); + }); + }); +```` + +Keep `const sleep = …`. It is still used later (`await sleep(320)`). + +- [ ] **Step 5: Run the tests and see them pass** + +Run: `node test/test-rendered-landing.js; echo "exit $?"` +Expected: the pass line and `exit 0`. + +Run: `printf '#!/bin/sh\necho "fake-chrome-startup-failure" >&2\nexit 3\n' > "$TMPDIR/fc" && chmod +x "$TMPDIR/fc" && CHROME_PATH="$TMPDIR/fc" node test/test-rendered-landing.js; echo "exit $?"` +Expected: in under 2 s, `rendered landing page test could not run: Chrome stopped (exit code 3) before it opened a debugging port. Last output: fake-chrome-startup-failure`, then `exit 1`. + +Run: `./test/smoke-test.sh 2>&1 | grep -E "FAIL|Results"` +Expected: `Results: N/N passed, 0 failed`. + +- [ ] **Step 6: Add mutation case 50** + +Insert this block directly before the mutation-test footer (see Global Constraints): + +````bash +# 50. The rendered landing test stops watching for a Chrome that stops at startup -> +# smoke-test must fail. Then that Chrome costs the full startup wait and the error +# does not give its exit code. Two CI failures left no clue in this way. +fresh +python3 - "$WORK/r/test/test-rendered-landing.js" <<'PY' +import sys +p = sys.argv[1]; s = open(p, encoding='utf-8').read() +old = " chrome.on('exit', (code, signal) => {\n" +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, " chrome.on('no-such-event', (code, signal) => {\n", 1) +open(p, 'w', encoding='utf-8').write(s) +PY +expect_fail "the rendered landing test ignores a Chrome that stops at startup" "$WORK/r/test/smoke-test.sh" "$WORK/r" +```` + +Run: `bash -n test/mutation-test.sh && echo ok` +Expected: `ok`. The full mutation run is in Task 5. This case adds about 30 s, because the mutated copy waits the full deadline. + +- [ ] **Step 7: Commit** + +```bash +git add test/test-rendered-landing.js test/smoke-test.sh test/mutation-test.sh +git commit -m "test: make the rendered landing test survive a slow Chrome start + +Use port 0 and the DevTools line on stderr, wait 30s, and fail at once +with the exit code and last output when Chrome stops early. Mutation 50 +guards the early-exit check. + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 2: The resolve chain finds the loaded plugin, and a missing checker is loud + +**Why:** Claude Code never sets `CLAUDE_PLUGIN_ROOT` in the Bash tool's shell. In plugin skill text it replaces only the exact token `${CLAUDE_PLUGIN_ROOT}` with the plugin path. The docs say so, and `claude -p --plugin-dir` probes on 2.1.289 confirmed it for slash-command and Skill-tool runs. + +idstack writes `"${CLAUDE_PLUGIN_ROOT:-}"`, which is never replaced. Every `$_IDSTACK/bin/...` call therefore falls through to the newest directory in `~/.claude/plugins/cache/idstack/idstack`. On the maintainer's machine that is a stale 3.5.1.0 copy with no `bin/idstack-ste-check`. This happens even though the loaded plugin is the 3.6.0.0 checkout (a directory marketplace) or a `--plugin-dir`. + +The writing check step also folds "no checker at that install" into the same `STE_CHECK_UNAVAILABLE` token as "no python3". The user only hears "the check did not run". + +The fix: +- Use the exact token in all six chain copies. +- When the token is not replaced (outside a plugin), bash sees an unset variable and the old order applies. +- Give the missing-checker case its own token, `STE_CHECK_MISSING: `, and a preamble rule. + +**Files:** +- Modify: `templates/snippets/idstack-resolve.sh` (header comment and the `for _p in` line) +- Modify: `templates/preamble.md` (header comment copy, 4 chain copies including the `for _dir in` consensus copy, rule 5 under "How to use the standard") +- Modify: `templates/manifest-schema.md` (header comment copy, chain copy) +- Modify: the 14 check-step lines in `skills/{accessibility-review,assessment-design,course-builder(x2),course-export(x2),course-import,course-quality-review,learn,learning-objectives,needs-analysis,pipeline,red-team(x2)}/SKILL.md.tmpl` +- Modify: `skills/learn/SKILL.md.tmpl` and `skills/course-export/SKILL.md.tmpl` (the prose after their check step) +- Modify: `test/smoke-test.sh` (two inserted blocks), `test/mutation-test.sh` (48a and 48c anchors, new cases 51a–51c), `CLAUDE.md` (resolve-order note) +- Regenerate: all 11 `skills/*/SKILL.md` + +**Interfaces:** +- Produces: the chain line `for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do`. The consensus copy uses `_dir`. +- Produces: the check-step shape `if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ARG; fi`. Each step stays on one line, because smoke counts check steps by line. +- Produces: preamble rules 5 (`STE_CHECK_UNAVAILABLE`) and 6 (`STE_CHECK_MISSING:`) under "How to use the standard". Tasks 3 and 4 do not depend on them. + +- [ ] **Step 1: Write the failing regression checks** + +In `test/smoke-test.sh`, directly after the line that starts with `check "no legacy .claude/plugins/idstack path in skill templates"`, insert: + +````bash +# Claude Code writes the plugin root into a skill's text only where it finds the +# exact token ${CLAUDE_PLUGIN_ROOT}. The Bash tool's shell does not have the +# variable, and a token with a default (":-") stays as it is. The chain used +# that form until this check, so every skill fell through to the newest cache +# dir: under --plugin-dir, and for a marketplace added from a local path, that +# was an old install with no bin/idstack-ste-check. Do to the generated text +# what Claude Code does, then run the first chain with a stale cache present. +RESOLVE_SIM=$(mktemp -d) +mkdir -p "$RESOLVE_SIM/plugin" "$RESOLVE_SIM/home/.claude/plugins/cache/idstack/idstack/0.0.1.0" +RESOLVE_SIM_GOT=$({ awk '/^_IDSTACK=""$/{f=1} f{print} f && /^done$/{exit}' "$IDSTACK_DIR/skills/learn/SKILL.md" \ + | sed "s|\${CLAUDE_PLUGIN_ROOT}|$RESOLVE_SIM/plugin|g"; echo 'printf "%s" "$_IDSTACK"'; } \ + | env -u CLAUDE_PLUGIN_ROOT -u IDSTACK_HOME HOME="$RESOLVE_SIM/home" bash) +check "the substituted plugin root wins over a stale cache install" "[ '$RESOLVE_SIM_GOT' = '$RESOLVE_SIM/plugin' ]" +rm -rf "$RESOLVE_SIM" +# The x3 count above pins only the preamble's "for _p in" copies. The consensus +# block ("for _dir in") and the longhand copy in manifest-schema.md need this. +check "no resolve chain gives the plugin root a default" "! grep -rlF '\${CLAUDE_PLUGIN_ROOT:-' '$IDSTACK_DIR/templates' $IDSTACK_DIR/skills/*/SKILL.md.tmpl $IDSTACK_DIR/skills/*/SKILL.md" +```` + +Directly after the `done` that closes the loop whose last check is `check "$skill SKILL.md.tmpl runs the ste checker at all $want check steps" "[ '$got' -eq $want ]"`, insert: + +````bash +# A check step must tell an install with no checker apart from a missing +# python3. Under --plugin-dir the chain once found a 3.5.1.0 install with no +# checker, and the step printed the python3 token. The skill then said only +# "the check did not run", and nothing told the user that the install was old. +STE_NOCHK=$(mktemp -d) +for skill in $SKILLS; do + check "$skill check steps name an install that has no checker" \ + "grep -F '\"\$_IDSTACK/bin/idstack-ste-check\"' '$IDSTACK_DIR/skills/$skill/SKILL.md.tmpl' | while IFS= read -r l; do _IDSTACK='$STE_NOCHK' bash -c \"\$l\" | grep -qxF 'STE_CHECK_MISSING: $STE_NOCHK' || exit 1; done" +done +rm -rf "$STE_NOCHK" +```` + +- [ ] **Step 2: Run them and see them fail** + +Run: `./test/smoke-test.sh 2>&1 | grep -E "FAIL|Results"` +Expected: 13 FAILs. +- `the substituted plugin root wins over a stale cache install` +- `no resolve chain gives the plugin root a default` +- one ` check steps name an install that has no checker` for each of the 11 skills + +No other FAIL. + +- [ ] **Step 3: Fix the resolve chain in all six copies and the header comment** + +Run this from the worktree root: + +````bash +python3 - <<'PY' +import io +def edit(path, pairs): + s = io.open(path, encoding="utf-8").read() + for old, new, n in pairs: + assert s.count(old) == n, "%s: %r occurs %d times, want %d" % (path, old, s.count(old), n) + s = s.replace(old, new) + io.open(path, "w", encoding="utf-8").write(s) + +TOKEN = ('"${CLAUDE_PLUGIN_ROOT:-}"', '"${CLAUDE_PLUGIN_ROOT}"') +OLD_HDR = ("# available here. Priority: explicit env overrides, then the Claude Code\n" + "# marketplace cache. Empty if none found; guard \"$_IDSTACK/bin/...\" calls\n" + "# accordingly.\n") +NEW_HDR = ("# available here. Priority: the plugin root that Claude Code writes into the\n" + "# skill text, then IDSTACK_HOME, then the Claude Code marketplace cache. Empty\n" + "# if none found; guard \"$_IDSTACK/bin/...\" calls accordingly.\n") +# The preamble's copy of the header wraps differently and says "(highest version)". +OLD_HDR_PRE = ("# available here. Priority: explicit env overrides, then the Claude Code\n" + "# marketplace cache (highest version). Empty if none found; guard\n" + "# \"$_IDSTACK/bin/...\" calls accordingly.\n") +NEW_HDR_PRE = ("# available here. Priority: the plugin root that Claude Code writes into the\n" + "# skill text, then IDSTACK_HOME, then the Claude Code marketplace cache\n" + "# (highest version). Empty if none found; guard \"$_IDSTACK/bin/...\" calls\n" + "# accordingly.\n") +CHAIN_OLD = 'for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do\n' +CHAIN_NEW = ("# CLAUDE_PLUGIN_ROOT has no \":-\" default on purpose. Claude Code replaces only\n" + "# the exact braced token in skill text. The Bash tool's shell does not have it.\n" + + CHAIN_OLD) + +edit("templates/snippets/idstack-resolve.sh", [TOKEN + (1,), (OLD_HDR, NEW_HDR, 1)]) +edit("templates/snippets/idstack-resolve.sh", [(CHAIN_OLD, CHAIN_NEW, 1)]) +edit("templates/preamble.md", [TOKEN + (4,), (OLD_HDR_PRE, NEW_HDR_PRE, 1)]) +edit("templates/manifest-schema.md", [TOKEN + (1,), (OLD_HDR, NEW_HDR, 1)]) +print("resolve chain fixed") +PY +grep -rn 'CLAUDE_PLUGIN_ROOT:-' templates skills/*/SKILL.md.tmpl || echo "no default left" +```` + +Expected: `resolve chain fixed`, then `no default left`. The header comment's semicolon is in a code comment that the model reads, not text shown to a person. It is unchanged from the old comment. + +- [ ] **Step 4: Give the check steps the distinct token** + +````bash +python3 - <<'PY' +import glob, io, re +OLD = re.compile(r'if \[ -x "\$_IDSTACK/bin/idstack-ste-check" \] && command -v python3 >/dev/null 2>&1; ' + r'then "\$_IDSTACK/bin/idstack-ste-check" (.+?); else echo "STE_CHECK_UNAVAILABLE"; fi') +NEW = (r'if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; ' + r'elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; ' + r'else "$_IDSTACK/bin/idstack-ste-check" \1; fi') +total = 0 +for p in sorted(glob.glob("skills/*/SKILL.md.tmpl")): + s = io.open(p, encoding="utf-8").read() + s2, n = OLD.subn(lambda m: m.expand(NEW), s) + total += n + io.open(p, "w", encoding="utf-8").write(s2) +assert total == 14, "replaced %d check steps, want 14" % total +print("14 check steps updated") +PY +```` + +Expected: `14 check steps updated`. + +- [ ] **Step 5: Add the preamble rules and update the two skills' prose** + +````bash +python3 - <<'PY' +import io +def edit(path, old, new): + s = io.open(path, encoding="utf-8").read() + assert s.count(old) == 1, "%s: anchor occurs %d times" % (path, s.count(old)) + io.open(path, "w", encoding="utf-8").write(s.replace(old, new, 1)) +edit("templates/preamble.md", + "5. If the checker does not run, continue the skill. Tell the user one time that the check did not run.\n", + '5. If the output is `STE_CHECK_UNAVAILABLE`, python3 is not available. Continue the skill. Tell the user one time that the check did not run.\n6. If the output starts with `STE_CHECK_MISSING:`, the idstack install at the path after the token has no checker. Continue the skill. Tell the user one time that the check did not run. Give the user the path and tell the user to update idstack. If the path is empty, tell the user that idstack did not find its install.\n') +edit("skills/learn/SKILL.md.tmpl", + 'If the output is `STE_CHECK_UNAVAILABLE`, tell the user one time that the check did not run. If the checker shows problems, show them to the user. Do not change the export file.', + 'If the output is `STE_CHECK_UNAVAILABLE` or starts with `STE_CHECK_MISSING:`, do step 5 or step 6 of "How to use the standard" in the preamble. If the checker shows problems, show them to the user. Do not change the export file.') +edit("skills/course-export/SKILL.md.tmpl", + '1. If the output is `STE_CHECK_UNAVAILABLE`, tell the user one time that the check did not run. Continue the export.', + '1. If the output is `STE_CHECK_UNAVAILABLE` or starts with `STE_CHECK_MISSING:`, do step 5 or step 6 of "How to use the standard" in the preamble. Continue the export.') +print("prose updated") +PY +```` + +Expected: `prose updated`. The preamble rules are outside the `ste-core` markers, so `extension/shared/ste-rules.js` does not change. + +- [ ] **Step 6: Update the CLAUDE.md resolve-order note** + +In `CLAUDE.md`, replace `the snippet is the single definition of that resolution order (\`CLAUDE_PLUGIN_ROOT\`, \`IDSTACK_HOME\`, then the Claude Code marketplace cache). \`templates/manifest-schema.md\` is spliced verbatim and so writes the resolution out longhand — smoke-test keeps the two in lockstep.` with: + +```text +the snippet is the single definition of that resolution order (`${CLAUDE_PLUGIN_ROOT}`, `IDSTACK_HOME`, then the Claude Code marketplace cache). Claude Code writes the plugin path into skill text only where it finds the exact token `${CLAUDE_PLUGIN_ROOT}`, and the Bash tool's shell never has the variable, so never give it a `:-` default. `templates/manifest-schema.md` is spliced verbatim and so writes the resolution out longhand — smoke-test keeps the two in lockstep and fails on any `${CLAUDE_PLUGIN_ROOT:-` in templates or skills. +``` + +- [ ] **Step 7: Regenerate, check the wording, run the tests** + +Run: `bin/idstack-gen-skills && bin/idstack-gen-skills --dry-run | tail -1` +Expected: the script writes 11 skill files, and the dry run reports them up to date. + +Run: `grep -E '^(5|6)\. If the output' templates/preamble.md | bin/idstack-ste-check --format markdown -` +Expected: `no problems`. Check only the two new rules. The full section holds the word list, and the word list names the words that it forbids. + +Run: `./test/smoke-test.sh 2>&1 | grep -E "FAIL|Results"; ./test/integration-test.sh 2>&1 | tail -1; ./test/test-preamble-python.sh 2>&1 | tail -1` +Expected: smoke `0 failed`, integration 51/51, preamble-python all pass. + +- [ ] **Step 8: Update the mutation anchors that quote the old check step, and add cases 51a–51c** + +Mutations 48a and 48c quote the old check-step text verbatim, and their anchor asserts fail until they are updated. Run: + +````bash +python3 - <<'PY' +import io +p = "test/mutation-test.sh" +s = io.open(p, encoding="utf-8").read() +def rep(old, new): + global s + assert s.count(old) == 1, (s.count(old), old[:90]) + s = s.replace(old, new, 1) +# 48a anchor: learn's check step line and the prose after it. +rep(""" 'if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ' + '".idstack/learnings-export.md"; else echo "STE_CHECK_UNAVAILABLE"; fi\\n```\\n\\n' + 'If the output is `STE_CHECK_UNAVAILABLE`, tell the user one time that the check did not run. ' + 'If the checker shows problems, show them to the user. Do not change the export file.\\n\\n')""", +""" 'if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; ' + 'elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; ' + 'else "$_IDSTACK/bin/idstack-ste-check" ".idstack/learnings-export.md"; fi\\n```\\n\\n' + 'If the output is `STE_CHECK_UNAVAILABLE` or starts with `STE_CHECK_MISSING:`, do step 5 or step 6 ' + 'of "How to use the standard" in the preamble. ' + 'If the checker shows problems, show them to the user. Do not change the export file.\\n\\n')""") + +# 48c anchor: course-builder's course-content check step. +rep(""" 'if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ' + '".idstack/course-content"; else echo "STE_CHECK_UNAVAILABLE"; fi\\n```\\n')""", +""" 'if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; ' + 'elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; ' + 'else "$_IDSTACK/bin/idstack-ste-check" ".idstack/course-content"; fi\\n```\\n')""") +io.open(p, "w", encoding="utf-8").write(s) +print("48a and 48c anchors updated") +PY +```` + +Then insert this block directly before the mutation-test footer: + +````bash +# 51a-51c. Claude Code writes the plugin root into skill text only where it finds +# the exact token ${CLAUDE_PLUGIN_ROOT}. The Bash tool's shell does not have the +# variable. With a ":-" default the chain fell through to an old cache install that +# had no checker, and the check step said only that the check did not run. +# 51a. The chain gives the plugin root a ":-" default again in all five copies -> +# smoke-test must fail. All copies move together, so the x3 lockstep count passes +# and only the substitution checks can see it. +fresh +python3 - "$WORK/r" <<'PY' +import sys +r = sys.argv[1] +for rel, n in (("templates/snippets/idstack-resolve.sh", 1), ("templates/preamble.md", 4), + ("templates/manifest-schema.md", 1)): + p = r + "/" + rel; s = open(p, encoding='utf-8').read() + old = '"${CLAUDE_PLUGIN_ROOT}"' + assert s.count(old) == n, '%s: anchor count %d' % (rel, s.count(old)) + s = s.replace(old, '"${CLAUDE_PLUGIN_ROOT:-}"') + open(p, 'w', encoding='utf-8').write(s) +PY +regen +expect_fail "the resolve chain gives the plugin root a default again" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 51b. Only the longhand copy in manifest-schema.md gets the ":-" default again -> +# smoke-test must fail. The x3 count and the substitution run read only the +# preamble's copies, so only the repo-wide grep sees this copy. +fresh +python3 - "$WORK/r/templates/manifest-schema.md" <<'PY' +import sys +p = sys.argv[1]; s = open(p, encoding='utf-8').read() +old = 'for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do' +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, 'for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do', 1) +open(p, 'w', encoding='utf-8').write(s) +PY +regen +expect_fail "the manifest-schema resolve chain gives the plugin root a default" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 51c. learn's check step folds a missing checker into the python3 token again -> +# smoke-test must fail. The step still calls the checker, so the count of check +# steps passes, and only the missing-checker run sees it. +fresh +python3 - "$WORK/r/skills/learn/SKILL.md.tmpl" <<'PY' +import sys +p = sys.argv[1]; s = open(p, encoding='utf-8').read() +old = ('if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; ' + 'elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; ' + 'else "$_IDSTACK/bin/idstack-ste-check" ".idstack/learnings-export.md"; fi') +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, 'if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; ' + 'then "$_IDSTACK/bin/idstack-ste-check" ".idstack/learnings-export.md"; ' + 'else echo "STE_CHECK_UNAVAILABLE"; fi', 1) +open(p, 'w', encoding='utf-8').write(s) +PY +regen +expect_fail "a check step folds a missing checker into the python3 token" "$WORK/r/test/smoke-test.sh" "$WORK/r" +```` + +Run: `bash -n test/mutation-test.sh && echo ok` +Expected: `ok`. + +- [ ] **Step 9: Commit** + +```bash +git add templates/snippets/idstack-resolve.sh templates/preamble.md templates/manifest-schema.md skills CLAUDE.md test/smoke-test.sh test/mutation-test.sh +git commit -m "fix: resolve the loaded plugin and report a missing checker + +Claude Code replaces only the exact token \${CLAUDE_PLUGIN_ROOT} in skill +text and never sets it in the Bash shell. The \":-\" form was never +replaced, so every skill ran the newest cached install, a stale one on +directory-marketplace and --plugin-dir setups. Check steps now print +STE_CHECK_MISSING with the path when that install has no checker. +Mutations 51a-51c guard the fix. + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 3: The report folder comes from the session's course title + +**Why:** +- **needs-analysis.** Step 6 (Generate Report) reads `project_name` from `.idstack/project.json` to build the export folder slug. Step 7 is the first step that writes the manifest. On a first run the slug is `untitled-course`, but every later skill writes to `exports//`. `bin/idstack-status` and the pipeline dashboard then miss the needs-analysis report. +- **course-import.** It has the same bug. Step 5 makes the report "before writing the manifest", and Step 6 writes `project_name`. A first import usually has no manifest, so it hits more often. + +The fix takes the slug from the course title of the session. That is the same text that the manifest step then writes as `project_name`. The steps are not reordered, because `report_path` goes into the single manifest write and other step text refers to step numbers. Both skills keep Read-modify-Write, as `CLAUDE.md` requires. + +The title goes through a top-level `IFS= read -r … <<'IDSTACK_PROJECT_NAME'` heredoc, not through `$(cat <<…)`. Inside `$(…)`, bash 3.2 fails on a title with an apostrophe. + +**Files:** +- Modify: `skills/needs-analysis/SKILL.md.tmpl` (Step 6 slug block, Step 7 list) +- Modify: `skills/course-import/SKILL.md.tmpl` (Step 5 slug block, Step 6 `project_name` bullet) +- Modify: `test/smoke-test.sh` (after the `learning_preferences_note has the same words…` check), `test/mutation-test.sh` (cases 52a–52b) +- Regenerate: `skills/needs-analysis/SKILL.md`, `skills/course-import/SKILL.md` + +**Interfaces:** +- Produces: in both templates, exactly one bash block that calls `idstack-slugify`. That block contains `` and not `.idstack/project.json`. The smoke check and mutations 52a–52b depend on this. + +- [ ] **Step 1: Write the failing regression check** + +In `test/smoke-test.sh`, directly after the line `check "learning_preferences_note has the same words in manifest-schema.md and needs-analysis" "python3 -c '$NOTE_SYNC_PY' '$IDSTACK_DIR'"`, insert: + +````bash +# needs-analysis and course-import write project_name, and they write the +# report before the manifest. A slug read from the manifest at that point is +# empty on a first run: the report went to exports/untitled-course/ and every +# later skill wrote to exports//. The slug must come from the +# course title of the session, the same text the skill then writes as project_name. +SLUG_SOURCE_PY='import re, sys +s = open(sys.argv[1], encoding="utf-8").read() +blocks = [b for b in re.findall(r"```bash\n(.*?)```", s, re.S) if "idstack-slugify" in b] +ok = len(blocks) == 1 and ".idstack/project.json" not in blocks[0] and "" in blocks[0] +sys.exit(0 if ok else 1)' +for skill in needs-analysis course-import; do + check "$skill takes the report slug from the course title, not the manifest it writes later" "python3 -c '$SLUG_SOURCE_PY' '$IDSTACK_DIR/skills/$skill/SKILL.md.tmpl'" +done +```` + +- [ ] **Step 2: Run it and see it fail** + +Run: `./test/smoke-test.sh 2>&1 | grep -E "FAIL|Results"` +Expected: exactly two FAILs, `needs-analysis takes the report slug from the course title, not the manifest it writes later` and `course-import takes the report slug from the course title, not the manifest it writes later`. + +- [ ] **Step 3: Change the slug blocks and tie `project_name` to them** + +````bash +python3 - <<'PY' +import io +def edit(path, old, new): + s = io.open(path, encoding="utf-8").read() + assert s.count(old) == 1, "%s: anchor occurs %d times" % (path, s.count(old)) + io.open(path, "w", encoding="utf-8").write(s.replace(old, new, 1)) +edit("skills/needs-analysis/SKILL.md.tmpl", +'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).\n\n```bash\n{{IDSTACK_RESOLVE}}\n# Compute the course slug from project_name and prepare the export folder.\n_PROJECT_NAME=$(python3 -c "import json; print(json.load(open(\'.idstack/project.json\')).get(\'project_name\',\'\'))" 2>/dev/null || echo "")\n_SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course")', +'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).\n\nIn this command, replace `` with the course title from Step 1. Then run the command.\n\n```bash\n{{IDSTACK_RESOLVE}}\n# Compute the course slug from this session\'s course title and prepare the export folder.\n# Do not read project_name from the manifest here. Step 7 writes it, so on a first run\n# the manifest has no name yet and the report lands in exports/untitled-course/.\nIFS= read -r _PROJECT_NAME <<\'IDSTACK_PROJECT_NAME\'\n\nIDSTACK_PROJECT_NAME\n_SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course")') +edit("skills/needs-analysis/SKILL.md.tmpl", +'6. If this is a new manifest, initialize ALL sections (including learning_objectives\n and quality_review) with empty/default values so downstream skills find the\n expected structure.', +'6. If this is a new manifest, initialize ALL sections (including learning_objectives\n and quality_review) with empty/default values so downstream skills find the\n expected structure.\n7. Set `project_name` to the text that you put in place of `` in\n Step 6. The next skills use `project_name` to find the report folder.') +edit("skills/course-import/SKILL.md.tmpl", +'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`.\n\n```bash\n{{IDSTACK_RESOLVE}}\n# Compute the course slug from project_name and prepare the export folder.\n_PROJECT_NAME=$(python3 -c "import json; print(json.load(open(\'.idstack/project.json\')).get(\'project_name\',\'\'))" 2>/dev/null || echo "")\n_SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course")', +'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`.\n\nIn this command, replace `` with the course title from the import. Then run the command.\n\n```bash\n{{IDSTACK_RESOLVE}}\n# Compute the course slug from this session\'s course title and prepare the export folder.\n# Do not read project_name from the manifest here. Step 6 writes it, so on a first import\n# the manifest has no name yet and the report lands in exports/untitled-course/.\nIFS= read -r _PROJECT_NAME <<\'IDSTACK_PROJECT_NAME\'\n\nIDSTACK_PROJECT_NAME\n_SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course")') +edit("skills/course-import/SKILL.md.tmpl", +'- `project_name` — from course title', +'- `project_name` — from course title. Use the text that you put in place of `` in Step 5.') +print("slug blocks updated") +PY +```` + +Expected: `slug blocks updated`. + +- [ ] **Step 4: Regenerate and run the tests** + +Run: `bin/idstack-gen-skills && ./test/smoke-test.sh 2>&1 | grep -E "FAIL|Results"` +Expected: `0 failed`. + +Then prove the block works end to end in bash 3.2. Use a title that has an apostrophe. Save this as `$TMPDIR/slug-e2e.sh` and run `bash "$TMPDIR/slug-e2e.sh"`: + +````bash +#!/bin/bash +C=/Users/philippossavvides/github/idstack/.claude/worktrees/feat-ste-output +D=$(mktemp -d) +cd "$D" || exit 1 +BLOCK=$(python3 - "$C/skills/needs-analysis/SKILL.md" <<'PY' +import re, sys +s = open(sys.argv[1], encoding="utf-8").read() +b = [b for b in re.findall(r"```bash\n(.*?)```", s, re.S) if "idstack-slugify" in b][0] +print(b.replace("", "Bob's Intro to Biology 101")) +PY +) +IDSTACK_HOME="$C" /bin/bash -c "$BLOCK" +echo "exit $?" +```` + +Expected: `Report path: .idstack/exports/bob-s-intro-to-biology-101/needs-analysis.html`, then `exit 0`. No `untitled-course`. (Verified in a dry run of this plan with GNU bash 3.2.57.) + +Run: `printf 'In this command, replace `` with the course title from Step 1. Then run the command.\nSet `project_name` to the text that you put in place of `` in Step 6. The next skills use `project_name` to find the report folder.\n' | bin/idstack-ste-check --format markdown -` +Expected: `no problems`. + +- [ ] **Step 5: Add mutation cases 52a–52b** + +Insert this block directly before the mutation-test footer: + +````bash +# 52a-52b. needs-analysis and course-import write project_name, and they write the +# report before the manifest. A slug that comes from the manifest is empty on a +# first run, so the report goes to exports/untitled-course/ and each later skill +# writes to exports//. Each skill has its own case, so a check that +# only reads the first skill cannot report GUARDED. +# 52a. needs-analysis reads the report slug from the manifest again -> smoke-test must fail. +fresh +python3 - "$WORK/r/skills/needs-analysis/SKILL.md.tmpl" <<'PY' +import sys +p = sys.argv[1]; s = open(p, encoding='utf-8').read() +old = "IFS= read -r _PROJECT_NAME <<'IDSTACK_PROJECT_NAME'\n\nIDSTACK_PROJECT_NAME\n" +new = '''_PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "")\n''' +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, new, 1) +open(p, 'w', encoding='utf-8').write(s) +PY +regen +expect_fail "needs-analysis reads the report slug from the manifest" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 52b. course-import reads the report slug from the manifest again -> smoke-test must fail. +fresh +python3 - "$WORK/r/skills/course-import/SKILL.md.tmpl" <<'PY' +import sys +p = sys.argv[1]; s = open(p, encoding='utf-8').read() +old = "IFS= read -r _PROJECT_NAME <<'IDSTACK_PROJECT_NAME'\n\nIDSTACK_PROJECT_NAME\n" +new = '''_PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "")\n''' +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, new, 1) +open(p, 'w', encoding='utf-8').write(s) +PY +regen +expect_fail "course-import reads the report slug from the manifest" "$WORK/r/test/smoke-test.sh" "$WORK/r" +```` + +Run: `bash -n test/mutation-test.sh && echo ok` +Expected: `ok`. + +- [ ] **Step 6: Commit** + +```bash +git add skills/needs-analysis skills/course-import test/smoke-test.sh test/mutation-test.sh +git commit -m "fix: name the report folder from the course title on a first run + +needs-analysis and course-import read project_name from the manifest +before they write it, so a first run put the report in +exports/untitled-course/. Take the slug from the session's course title, +the same text the manifest step writes. Mutations 52a-52b guard the fix. + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 4: Citations state the tier that references.md gives them + +**Why:** +- **Wrong tiers.** Four skill templates cite the two Sweller papers as T1. `evidence/references.md` files them as T5: `[CogLoad-4]` is Sweller 1994 and `[CogLoad-19]` is Sweller 2024. That is 15 wrong citations, and reports copy them. `course-builder` also has `[Multimedia-6] [CogLoad-6] [T1] [T3]`, with the tiers in swapped positions: Multimedia-6 is T3 and CogLoad-6 is T1. +- **No check.** Nothing compares skill citations with `references.md`. `test/check-evidence-cards.py` covers only the landing cards, and `test/test-evidence-labels.mjs` covers only `extension/`. +- **No correction at run time.** `bin/idstack-consensus verify` corrects a tier at run time, but needs-analysis, course-builder and learning-objectives do not call it. +- **The new checker.** The repo's rule is "a code always has its references.md tier", the same rule that `idstack-consensus` and `test-evidence-labels.mjs` apply. So a new sibling checker applies it to skills, `templates/`, `README.md` and `docs/index.html`. +- **One wording change.** `references.md` has no T1 or T2 expertise-reversal source, so the needs-analysis "so the read isn't opinion" clause is changed, not only re-tiered. + +**Files:** +- Create: `test/check-citation-tiers.py` (executable) +- Modify: `skills/needs-analysis/SKILL.md.tmpl` (5 lines), `skills/course-builder/SKILL.md.tmpl` (6), `skills/course-quality-review/SKILL.md.tmpl` (1), `skills/learning-objectives/SKILL.md.tmpl` (1) +- Modify: `test/smoke-test.sh` (before `check "doc accuracy check passes"`), `test/mutation-test.sh` (cases 53a–53c), `CLAUDE.md` (Tests list), `CONTRIBUTING.md` (test table) +- Regenerate: the four matching `skills/*/SKILL.md` + +**Interfaces:** +- Produces: `python3 test/check-citation-tiers.py `. It prints one problem per line and exits 1, or prints nothing and exits 0. It also exits 1 if it matches no citation at all, so a broken pattern cannot pass. Accepted forms: `[A-1] [T1]`, and `[A-1] [B-2] [T1]` (every code at that tier). It refuses two or more tiers after a group of codes. + +- [ ] **Step 1: Create the checker** + +Create `test/check-citation-tiers.py`, then run `chmod +x test/check-citation-tiers.py`: + +````python +#!/usr/bin/env python3 +"""Assert every "[Code-N] [Tn]" citation states the tier evidence/references.md gives it. + +Skills tell the model how to cite evidence, and the reports copy those citations. +needs-analysis, course-builder, course-quality-review and learning-objectives +cited the two Sweller papers ([CogLoad-4], [CogLoad-19]) as T1, but +references.md files both as T5. A citation that claims stronger evidence than +the repo holds is the error idstack can least afford, so the tier is read from +references.md here instead of trusted. bin/idstack-consensus applies the same +rule at run time: a code in references.md always gets its references.md tier. + +Forms checked, after HTML tags are removed: + [A-1] [T1] the code must be T1 in references.md + [A-1] [B-2] [T1] each code must be T1 in references.md + [A-1] [B-2] [T1] [T3] refused: put the tier after each code instead +Each [Code-N] must also be an entry in references.md. + +Scope: skills/*/SKILL.md.tmpl, templates/, README.md and docs/index.html. The +generated SKILL.md files are left out (they repeat the templates, and +smoke-test keeps them fresh). test/test-evidence-labels.mjs checks extension/. + +Usage: check-citation-tiers.py +Prints one line per problem and exits 1; prints nothing and exits 0 when clean. +Runs on Python 3.9 (the macOS system interpreter). +""" + +import glob +import io +import os +import re +import sys + +# "- [CogLoad-4] Sweller, J. (1994). ... *Learning and Instruction*. T5" +REF_RE = re.compile(r"^- \[([A-Za-z]+-\d+)\] .* (T[1-5])\s*$") +CODE_RE = re.compile(r"\[([A-Z][A-Za-z]*-\d+)\]") +# One or more codes, then one or more tiers. +GROUP_RE = re.compile(r"((?:\[[A-Z][A-Za-z]*-\d+\]\s*)+)((?:\[T[1-5]\]\s*)+)") +# The landing page shows a tier as T1. +TIER_SPAN_RE = re.compile(r'(T[1-5])') +TAG_RE = re.compile(r"<[^>]+>") + + +def load_refs(path): + refs = {} + for line in io.open(path, encoding="utf-8"): + m = REF_RE.match(line) + if m: + refs[m.group(1)] = m.group(2) + return refs + + +def sources(root): + paths = glob.glob(os.path.join(root, "skills", "*", "SKILL.md.tmpl")) + paths += glob.glob(os.path.join(root, "templates", "**", "*"), recursive=True) + paths += [os.path.join(root, "README.md"), os.path.join(root, "docs", "index.html")] + return sorted(p for p in paths if os.path.isfile(p)) + + +def plain(line): + line = TIER_SPAN_RE.sub(r"[\1]", line) + return TAG_RE.sub("", line).replace(" ", " ") + + +def main(): + if len(sys.argv) != 2: + print("usage: check-citation-tiers.py ") + return 2 + root = sys.argv[1] + refs_path = os.path.join(root, "evidence", "references.md") + if not os.path.isfile(refs_path): + print("missing file: %s" % refs_path) + return 1 + refs = load_refs(refs_path) + if not refs: + print("no '- [Code-N] ... Tn' entries found in evidence/references.md") + return 1 + + problems = [] + cited = 0 + for path in sources(root): + rel = os.path.relpath(path, root) + lines = io.open(path, encoding="utf-8").read().split("\n") + for n, raw in enumerate(lines, 1): + line = plain(raw) + where = "%s:%d" % (rel, n) + for code in CODE_RE.findall(line): + if code not in refs: + problems.append("%s: [%s] is not in evidence/references.md" % (where, code)) + for m in GROUP_RE.finditer(line): + codes = CODE_RE.findall(m.group(1)) + tiers = re.findall(r"T[1-5]", m.group(2)) + cited += 1 + if len(tiers) > 1: + problems.append( + "%s: %s gives more than one tier. Put the tier after each code." + % (where, m.group(0).strip()) + ) + continue + for code in codes: + if code in refs and refs[code] != tiers[0]: + problems.append( + "%s: [%s] is %s in evidence/references.md, not %s" + % (where, code, refs[code], tiers[0]) + ) + + # A pattern that stops matching would make every check above vacuous. + if cited == 0: + problems.append("no '[Code-N] [Tn]' citations found; the checker matched nothing") + + for line in problems: + print(line) + return 1 if problems else 0 + + +if __name__ == "__main__": + sys.exit(main()) +```` + +- [ ] **Step 2: Wire it into smoke-test** + +In `test/smoke-test.sh`, replace the anchor: + +````bash +check "doc accuracy check passes" "python3 '$IDSTACK_DIR/test/check-doc-accuracy.py' '$IDSTACK_DIR'" +```` + +with: + +````bash +# Skills and templates cite evidence as "[Code-N] [Tn]", and the reports copy +# those citations. Four skills cited the two Sweller papers ([CogLoad-4], +# [CogLoad-19]) as T1, but references.md files both as T5. The tier is read +# from the reference file, as for the cards above. test-evidence-labels.mjs +# checks the extension. Same python3 guard and crash handling as the cards. +TIER_DRIFT="" +if command -v python3 &>/dev/null; then + TIER_DRIFT="$(python3 "$IDSTACK_DIR/test/check-citation-tiers.py" "$IDSTACK_DIR" 2>&1)" \ + || TIER_DRIFT="citation-tier checker failed: +$TIER_DRIFT" + check "skill and template citations state their evidence/references.md tier" \ + "if [ -n \"\$TIER_DRIFT\" ]; then printf '%s\n' \"\$TIER_DRIFT\"; false; fi" +fi + +check "doc accuracy check passes" "python3 '$IDSTACK_DIR/test/check-doc-accuracy.py' '$IDSTACK_DIR'" +```` + +- [ ] **Step 3: Run the checker and see it fail** + +Run: `python3 test/check-citation-tiers.py .; echo "exit $?"` +Expected: `exit 1` and exactly these 16 lines. Line numbers can be different by a few lines after Tasks 2–3. The files, codes and tiers must match. + +````text +skills/course-builder/SKILL.md.tmpl:55: [CogLoad-4] is T5 in evidence/references.md, not T1 +skills/course-builder/SKILL.md.tmpl:63: [CogLoad-19] is T5 in evidence/references.md, not T1 +skills/course-builder/SKILL.md.tmpl:89: [CogLoad-4] is T5 in evidence/references.md, not T1 +skills/course-builder/SKILL.md.tmpl:89: [CogLoad-19] is T5 in evidence/references.md, not T1 +skills/course-builder/SKILL.md.tmpl:403: [CogLoad-4] is T5 in evidence/references.md, not T1 +skills/course-builder/SKILL.md.tmpl:403: [CogLoad-19] is T5 in evidence/references.md, not T1 +skills/course-builder/SKILL.md.tmpl:411: [CogLoad-19] is T5 in evidence/references.md, not T1 +skills/course-builder/SKILL.md.tmpl:427: [Multimedia-6] [CogLoad-6] [T1] [T3] gives more than one tier. Put the tier after each code. +skills/course-quality-review/SKILL.md.tmpl:505: [CogLoad-19] is T5 in evidence/references.md, not T1 +skills/learning-objectives/SKILL.md.tmpl:214: [CogLoad-19] is T5 in evidence/references.md, not T1 +skills/needs-analysis/SKILL.md.tmpl:40: [CogLoad-19] is T5 in evidence/references.md, not T1 +skills/needs-analysis/SKILL.md.tmpl:238: [CogLoad-19] is T5 in evidence/references.md, not T1 +skills/needs-analysis/SKILL.md.tmpl:307: [CogLoad-4] is T5 in evidence/references.md, not T1 +skills/needs-analysis/SKILL.md.tmpl:311: [CogLoad-19] is T5 in evidence/references.md, not T1 +skills/needs-analysis/SKILL.md.tmpl:343: [CogLoad-4] is T5 in evidence/references.md, not T1 +skills/needs-analysis/SKILL.md.tmpl:343: [CogLoad-19] is T5 in evidence/references.md, not T1 +```` + +Run: `/usr/bin/python3 test/check-citation-tiers.py . | wc -l` +Expected: `16`. This proves that the checker runs on Python 3.9. + +- [ ] **Step 4: Correct the citations** + +````bash +python3 - <<'PY' +import io +EDITS = [ + ('skills/needs-analysis/SKILL.md.tmpl', [ + (' What helps novices hurts experts (expertise reversal effect) [CogLoad-19] [T1].', + ' What helps novices hurts experts (expertise reversal effect) [CogLoad-19] [T5].'), + (' [CogLoad-19] [T1]. The entire sequencing, scaffolding, and assessment strategy', + ' [CogLoad-19] [T5]. The entire sequencing, scaffolding, and assessment strategy'), + (' structured guidance [CogLoad-4] [T1]', + ' structured guidance [CogLoad-4] [T5]'), + (' avoid redundant information that adds extraneous cognitive load [CogLoad-19] [T1]', + ' avoid redundant information that adds extraneous cognitive load [CogLoad-19] [T5]'), + ("Cite the expertise-reversal evidence so the read isn't opinion: `[CogLoad-4] [T1]` for novices; `[CogLoad-19] [T1]` for advanced; `[Learner-16] [T1]` for mixed.", + 'Cite the expertise-reversal evidence at its references.md tier: `[CogLoad-4] [T5]` for novices; `[CogLoad-19] [T5]` for advanced; `[Learner-16] [T1]` for mixed.'), + ]), + ('skills/course-builder/SKILL.md.tmpl', [ + (' within modules and the progression of complexity across a course [CogLoad-4] [T1].', + ' within modules and the progression of complexity across a course [CogLoad-4] [T5].'), + (" adapted to the audience's expertise level, not generated one-size-fits-all\n [CogLoad-19] [T1].", + " adapted to the audience's expertise level, not generated one-size-fits-all\n [CogLoad-19] [T5]."), + (' that activate existing schemas. Module activities must reflect this distinction\n [CogLoad-4] [CogLoad-19] [T1].', + ' that activate existing schemas. Module activities must reflect this distinction\n [CogLoad-4] [CogLoad-19] [T5].'), + ('**Novice learners** [CogLoad-4] [CogLoad-19] [T1]:', + '**Novice learners** [CogLoad-4] [CogLoad-19] [T5]:'), + ('**Advanced learners** [CogLoad-19] [T1]:', + '**Advanced learners** [CogLoad-19] [T5]:'), + ('**Segmenting and spacing** [Multimedia-6] [CogLoad-6] [T1] [T3]:', + '**Segmenting and spacing** [Multimedia-6] [T3] [CogLoad-6] [T1]:'), + ]), + ('skills/course-quality-review/SKILL.md.tmpl', [ + ('**Evidence:** [CogLoad-19] [T1]', + '**Evidence:** [CogLoad-19] [T5]'), + ]), + ('skills/learning-objectives/SKILL.md.tmpl', [ + (' already have this knowledge [CogLoad-19] [T1]. Recommend starting at apply or higher.', + ' already have this knowledge [CogLoad-19] [T5]. Recommend starting at apply or higher.'), + ]), +] +for path, pairs in EDITS: + s = io.open(path, encoding="utf-8").read() + for old, new in pairs: + assert s.count(old) == 1, "%s: %r occurs %d times" % (path, old, s.count(old)) + s = s.replace(old, new, 1) + io.open(path, "w", encoding="utf-8").write(s) +print("citations corrected") +PY +```` + +Expected: `citations corrected`. Leave `test/fixtures/ste/clean-report.html` and `test/test-ste-check.py` alone. Their T1 citations are word-count test data for the STE checker, and the tier checker does not read `test/`. + +- [ ] **Step 5: Document the checker** + +In `CLAUDE.md`, after the line `python3 test/check-evidence-cards.py . # Verifies landing page evidence cards match evidence/references.md`, add: + +```text +python3 test/check-citation-tiers.py . # Verifies each [Code-N] [Tn] in skills, templates, README and landing page has its references.md tier +``` + +In `CONTRIBUTING.md`, after the row `| `python3 test/check-evidence-cards.py .` | Verifies landing page evidence card study counts and tier ranges against `evidence/references.md` |`, add: + +```text +| `python3 test/check-citation-tiers.py .` | Verifies that each `[Code-N] [Tn]` citation in the skill templates, `templates/`, README and landing page states the tier that `evidence/references.md` gives that code. `test/smoke-test.sh` runs it | +``` + +- [ ] **Step 6: Regenerate and run the tests** + +Run: `bin/idstack-gen-skills && python3 test/check-citation-tiers.py .; echo "exit $?"` +Expected: no problem lines, `exit 0`. + +Run: `./test/smoke-test.sh 2>&1 | grep -E "FAIL|Results"; python3 test/check-doc-accuracy.py . && echo doc-ok` +Expected: `0 failed`, `doc-ok`. + +- [ ] **Step 7: Add mutation cases 53a–53c** + +Insert this block directly before the mutation-test footer: + +````bash +# 53a-53c. Skills cite evidence as "[Code-N] [Tn]" and the reports copy the +# citations. Four skills cited the two Sweller papers ([CogLoad-4], [CogLoad-19]) +# as T1, but references.md files both as T5. check-citation-tiers.py reads each +# tier from references.md; each case puts back one wrong form. +# 53a. needs-analysis cites [CogLoad-19] as T1 again -> smoke-test must fail. +fresh +python3 - "$WORK/r/skills/needs-analysis/SKILL.md.tmpl" <<'PY' +import sys +p = sys.argv[1]; s = open(p, encoding='utf-8').read() +old = "(expertise reversal effect) [CogLoad-19] [T5].\n" +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, "(expertise reversal effect) [CogLoad-19] [T1].\n", 1) +open(p, 'w', encoding='utf-8').write(s) +PY +regen +expect_fail "needs-analysis cites [CogLoad-19] as T1" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 53b. A group of codes with one tier claims T1 for two T5 papers -> smoke-test +# must fail. Each code in the group must be at the tier of the group. +fresh +python3 - "$WORK/r/skills/course-builder/SKILL.md.tmpl" <<'PY' +import sys +p = sys.argv[1]; s = open(p, encoding='utf-8').read() +old = "**Novice learners** [CogLoad-4] [CogLoad-19] [T5]:\n" +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, "**Novice learners** [CogLoad-4] [CogLoad-19] [T1]:\n", 1) +open(p, 'w', encoding='utf-8').write(s) +PY +regen +expect_fail "course-builder cites a group of T5 papers as T1" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 53c. Two codes are followed by two tiers in the wrong order again -> smoke-test +# must fail. The checker refuses this form, because the order of the tiers does +# not show which tier belongs to which code. +fresh +python3 - "$WORK/r/skills/course-builder/SKILL.md.tmpl" <<'PY' +import sys +p = sys.argv[1]; s = open(p, encoding='utf-8').read() +old = "**Segmenting and spacing** [Multimedia-6] [T3] [CogLoad-6] [T1]:\n" +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, "**Segmenting and spacing** [Multimedia-6] [CogLoad-6] [T1] [T3]:\n", 1) +open(p, 'w', encoding='utf-8').write(s) +PY +regen +expect_fail "course-builder gives two codes two tiers in one group" "$WORK/r/test/smoke-test.sh" "$WORK/r" +```` + +Run: `bash -n test/mutation-test.sh && echo ok` +Expected: `ok`. + +- [ ] **Step 8: Commit** + +```bash +git add test/check-citation-tiers.py skills CLAUDE.md CONTRIBUTING.md test/smoke-test.sh test/mutation-test.sh +git commit -m "fix: cite the Sweller papers at their references.md tier + +Four skills cited [CogLoad-4] and [CogLoad-19] as T1, but references.md +files both as T5, and course-builder gave two codes swapped tiers. The +new test/check-citation-tiers.py reads each tier from references.md, and +smoke-test runs it. Mutations 53a-53c guard the fix. + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 5: Record the smoke total and run every suite + +**Files:** +- Modify: `CLAUDE.md` (the smoke-test assertion count on the `./test/smoke-test.sh` line) + +- [ ] **Step 1: Measure the real smoke total** + +Run from this worktree, with no argument: `./test/smoke-test.sh 2>&1 | grep Results` +Expected: `Results: N/N passed, 0 failed`. N is about 401: 384, plus 1 (Task 1), 13 (Task 2), 2 (Task 3) and 1 (Task 4). Use the measured N, not the arithmetic. + +- [ ] **Step 2: Write N into CLAUDE.md** + +In `CLAUDE.md`, replace `./test/smoke-test.sh # 384 assertions:` with the same text and N in place of 384. Run `python3 test/check-doc-accuracy.py . && echo doc-ok`. +Expected: `doc-ok`. + +- [ ] **Step 3: Run every suite** + +```bash +./test/integration-test.sh 2>&1 | tail -1 +./test/test-setup.sh 2>&1 | tail -1 +./test/test-doctor.sh 2>&1 | tail -1 +./test/test-status.sh 2>&1 | tail -1 +./test/test-preamble-python.sh 2>&1 | tail -1 +./test/test-extension.sh 2>&1 | tail -1 +python3 test/test-ste-check.py 2>&1 | tail -1 +/usr/bin/python3 test/check-citation-tiers.py . && echo tiers-ok-3.9 +python3 test/check-evidence-cards.py . && echo cards-ok +``` + +Expected: every suite passes, and `tiers-ok-3.9` and `cards-ok` print. + +- [ ] **Step 4: Run the full mutation suite** + +Run: `./test/mutation-test.sh 2>&1 | tail -15` (about 15 minutes. Run it in the background and wait for it.) +Expected: `NOT guarded: 0` and `skipped: 0`. The guarded count is 129 + 9 = 138. These new labels each show `GUARDED`: +- `the rendered landing test ignores a Chrome that stops at startup` +- `the resolve chain gives the plugin root a default again` +- `the manifest-schema resolve chain gives the plugin root a default` +- `a check step folds a missing checker into the python3 token` +- `needs-analysis reads the report slug from the manifest` +- `course-import reads the report slug from the manifest` +- `needs-analysis cites [CogLoad-19] as T1` +- `course-builder cites a group of T5 papers as T1` +- `course-builder gives two codes two tiers in one group` + +- [ ] **Step 5: Commit** + +```bash +git add CLAUDE.md +git commit -m "docs: record the smoke-test total after the follow-up fixes + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 6: PR, CI on Linux, merge + +Two parts of this plan are verified only on macOS: +- Chrome printing `DevTools listening on` through the `/usr/bin/google-chrome` wrapper. +- The `awk`, `sed` and `env -u` calls in Task 2's smoke block on GNU tools. + +A green badge alone does not prove them. Read the logs. + +- [ ] **Step 1: Push and open the PR** + +Push `fix/post-ste-followups` and open a PR against `main`. Run `bin/idstack-ste-check --format markdown` on the PR body before you create the PR. The body must say: +- **What each task fixes.** +- **Behavior change.** In plugin runs, the plugin root now wins over `IDSTACK_HOME`. The documented order is the same, but before, the first entry was always empty. +- **Who was affected.** A GitHub-marketplace install was not affected, because its newest cache directory is its installed version. A directory marketplace or `--plugin-dir` ran the stale cached copy. So no release is needed. +- **Manual step after merge.** The maintainer's directory marketplace at `/Users/philippossavvides/github/idstack` is behind `main` and has no `bin/idstack-ste-check`. Until someone runs `git pull` there, check steps print `STE_CHECK_MISSING: /Users/philippossavvides/github/idstack`. This is the intended loud result. + +- [ ] **Step 2: Read the CI logs, not just the badges** + +Wait for all checks: two ubuntu legs (3.9, 3.12), macOS 3.12, mutation, lint. For each ubuntu test job, run `gh run view --log | grep -E "rendered landing|plugin root|no checker|Results:"`. +Expected: +- `PASS: rendered landing page tests pass` +- `PASS: rendered landing test reports why Chrome stopped at startup` +- `PASS: the substituted plugin root wins over a stale cache install` +- 11 `PASS: … check steps name an install that has no checker` lines +- `Results: N/N passed`, with N equal to the number in `CLAUDE.md` + +The mutation job must show `NOT guarded: 0`. + +If the rendered test fails on ubuntu with `did not open a debugging port within 30s`, the stderr tail in the message shows why. Stop and report. Do not add a retry. + +- [ ] **Step 3: Ask before you merge** + +The `main` ruleset requires one approving review. Ask the user to choose: admin merge (`gh pr merge --merge --admin`), auto-merge, or leave open. Approval for an earlier PR does not carry over. After a merge, delete the remote branch with `git push origin --delete fix/post-ste-followups`. Do not use `--delete-branch`, because this worktree cannot check out `main`. + +- [ ] **Step 4: Tell the user the manual step** + +Tell the user to run `git pull` in `/Users/philippossavvides/github/idstack` so their sessions get the checker. From f396ab4eed3eec155a843f10c07fb6fa7effb8c8 Mon Sep 17 00:00:00 2001 From: Philippos Savvides Date: Wed, 7 Oct 2026 16:54:49 -0700 Subject: [PATCH 2/7] test: make the rendered landing test survive a slow Chrome start Use port 0 and the DevTools line on stderr, wait 30s, and fail at once with the exit code and last output when Chrome stops early. Mutation 50 guards the early-exit check. Co-Authored-By: Claude Opus 5.5 --- test/mutation-test.sh | 14 ++++++++++++ test/smoke-test.sh | 11 ++++++++++ test/test-rendered-landing.js | 40 +++++++++++++++++++++++------------ 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/test/mutation-test.sh b/test/mutation-test.sh index acd4e45..d37549a 100755 --- a/test/mutation-test.sh +++ b/test/mutation-test.sh @@ -1841,6 +1841,20 @@ open(p, 'w', encoding='utf-8').write(s) PY expect_fail "a side-panel loader step writes non-STE text" "$WORK/r/test/smoke-test.sh" "$WORK/r" +# 50. The rendered landing test stops watching for a Chrome that stops at startup -> +# smoke-test must fail. Then that Chrome costs the full startup wait and the error +# does not give its exit code. Two CI failures left no clue in this way. +fresh +python3 - "$WORK/r/test/test-rendered-landing.js" <<'PY' +import sys +p = sys.argv[1]; s = open(p, encoding='utf-8').read() +old = " chrome.on('exit', (code, signal) => {\n" +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, " chrome.on('no-such-event', (code, signal) => {\n", 1) +open(p, 'w', encoding='utf-8').write(s) +PY +expect_fail "the rendered landing test ignores a Chrome that stops at startup" "$WORK/r/test/smoke-test.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 d9c186f..77778a9 100755 --- a/test/smoke-test.sh +++ b/test/smoke-test.sh @@ -362,6 +362,16 @@ if command -v node &>/dev/null; then # targets, column counts) rather than the CSS text. Skips loudly without a browser; the # text suite above still runs. See test/test-rendered-landing.js for why both exist. check "rendered landing page tests pass" "node '$IDSTACK_DIR/test/test-rendered-landing.js'" + # A Chrome that stops at startup must fail the test at once and give its exit code and last + # output. Before, the test waited 12s and said only that no debugging port opened, so two CI + # failures left no clue. Needs no real browser: CHROME_PATH points at a stub that stops. + FAKE_CHROME_DIR=$(mktemp -d) + printf '#!/bin/sh\necho "fake-chrome-startup-failure" >&2\nexit 3\n' > "$FAKE_CHROME_DIR/chrome" + chmod +x "$FAKE_CHROME_DIR/chrome" + check "rendered landing test reports why Chrome stopped at startup" \ + "CHROME_PATH='$FAKE_CHROME_DIR/chrome' node '$IDSTACK_DIR/test/test-rendered-landing.js'" \ + 1 "exit code 3.*fake-chrome-startup-failure" + rm -rf "$FAKE_CHROME_DIR" # The fixed text that the extension shows must obey ASD-STE100: the demo audits, # the Markdown export, the rendered findings, the error messages and the side-panel # page. pipefail: a printer that crashes must fail here, not send empty text. @@ -376,6 +386,7 @@ else echo " SKIP: chrome extension tests (node not installed)" echo " SKIP: responsive landing page tests (node not installed)" echo " SKIP: rendered landing page tests (node not installed)" + echo " SKIP: rendered landing startup report (node not installed)" echo " SKIP: extension STE checks (node not installed)" fi diff --git a/test/test-rendered-landing.js b/test/test-rendered-landing.js index 38fcff1..2fecff1 100755 --- a/test/test-rendered-landing.js +++ b/test/test-rendered-landing.js @@ -57,7 +57,9 @@ const problems = []; const sleep = ms => new Promise(r => setTimeout(r, ms)); const userDataDir = path.join(os.tmpdir(), `idstack-rendered-${process.pid}`); -const port = 9400 + (process.pid % 500); +// A cold Chrome on a fresh ubuntu runner has missed a 12s deadline. The wait costs time only +// when Chrome is stuck, so it is generous. +const STARTUP_SECONDS = 30; let chrome = null; function killChrome() { @@ -69,22 +71,34 @@ process.on('exit', killChrome); for (const sig of ['SIGINT', 'SIGTERM']) process.on(sig, () => { killChrome(); process.exit(1); }); async function main() { + // Port 0: Chrome picks a free port and prints its address on stderr, so no other process can + // hold the port. Keep reading stderr for the whole run. A full pipe stops Chrome. chrome = spawn(chromePath, [ - '--headless=new', `--remote-debugging-port=${port}`, + '--headless=new', '--remote-debugging-port=0', '--no-first-run', '--no-default-browser-check', '--disable-gpu', '--hide-scrollbars', '--no-sandbox', `--user-data-dir=${userDataDir}`, 'about:blank', - ], { stdio: 'ignore' }); - - let wsUrl = null; - for (let i = 0; i < 80; i++) { - try { - const res = await fetch(`http://127.0.0.1:${port}/json/version`); - wsUrl = (await res.json()).webSocketDebuggerUrl; - break; - } catch { await sleep(150); } - } - if (!wsUrl) throw new Error('Chrome did not expose a debugging port within 12s'); + ], { stdio: ['ignore', 'ignore', 'pipe'] }); + + let stderr = ''; + const lastOutput = () => stderr.trim().split('\n').slice(-3).join(' / '); + const wsUrl = await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error( + `Chrome did not open a debugging port within ${STARTUP_SECONDS}s. Last output: ${lastOutput()}`)), + STARTUP_SECONDS * 1000); + chrome.stderr.on('data', chunk => { + stderr += chunk; + const m = stderr.match(/DevTools listening on (ws:\/\/\S+)/); + if (m) { clearTimeout(timer); resolve(m[1]); } + stderr = stderr.slice(-2000); + }); + chrome.on('error', err => { clearTimeout(timer); reject(err); }); + chrome.on('exit', (code, signal) => { + clearTimeout(timer); + reject(new Error(`Chrome stopped (${signal || `exit code ${code}`}) before it opened ` + + `a debugging port. Last output: ${lastOutput()}`)); + }); + }); const ws = new WebSocket(wsUrl); await new Promise((res, rej) => { From 9a24b07cfbcf7808235766c6201e1f4fb7f0484d Mon Sep 17 00:00:00 2001 From: Philippos Savvides Date: Wed, 7 Oct 2026 17:00:22 -0700 Subject: [PATCH 3/7] fix: resolve the loaded plugin and report a missing checker Claude Code replaces only the exact token ${CLAUDE_PLUGIN_ROOT} in skill text and never sets it in the Bash shell. The ":-" form was never replaced, so every skill ran the newest cached install, a stale one on directory-marketplace and --plugin-dir setups. Check steps now print STE_CHECK_MISSING with the path when that install has no checker. Mutations 51a-51c guard the fix. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- skills/accessibility-review/SKILL.md | 88 ++++++++++-------- skills/accessibility-review/SKILL.md.tmpl | 2 +- skills/assessment-design/SKILL.md | 98 +++++++++++--------- skills/assessment-design/SKILL.md.tmpl | 2 +- skills/course-builder/SKILL.md | 90 ++++++++++-------- skills/course-builder/SKILL.md.tmpl | 4 +- skills/course-export/SKILL.md | 102 ++++++++++++--------- skills/course-export/SKILL.md.tmpl | 6 +- skills/course-import/SKILL.md | 68 ++++++++------ skills/course-import/SKILL.md.tmpl | 2 +- skills/course-quality-review/SKILL.md | 88 ++++++++++-------- skills/course-quality-review/SKILL.md.tmpl | 2 +- skills/learn/SKILL.md | 92 +++++++++++-------- skills/learn/SKILL.md.tmpl | 4 +- skills/learning-objectives/SKILL.md | 78 +++++++++------- skills/learning-objectives/SKILL.md.tmpl | 2 +- skills/needs-analysis/SKILL.md | 68 ++++++++------ skills/needs-analysis/SKILL.md.tmpl | 2 +- skills/pipeline/SKILL.md | 50 +++++----- skills/pipeline/SKILL.md.tmpl | 2 +- skills/red-team/SKILL.md | 102 ++++++++++++--------- skills/red-team/SKILL.md.tmpl | 4 +- templates/manifest-schema.md | 8 +- templates/preamble.md | 18 ++-- templates/snippets/idstack-resolve.sh | 10 +- test/mutation-test.sh | 69 +++++++++++++- test/smoke-test.sh | 27 ++++++ 28 files changed, 664 insertions(+), 426 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9793030..16fa41b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -120,7 +120,7 @@ Every skill template follows this pattern: 2. **`{{PREAMBLE}}`** placeholder (replaced by `templates/preamble.md` during generation) 3. **Workflow** (Markdown defining the conversational flow, decision trees, outputs) 4. **`{{MANIFEST_SCHEMA}}`** placeholder (replaced by `templates/manifest-schema.md`) -5. **`{{IDSTACK_RESOLVE}}`** placeholder (replaced by `templates/snippets/idstack-resolve.sh`). Unlike the other two, this one appears many times per template — once at the top of every bash block that calls `$_IDSTACK/bin/...`. Bash blocks run in separate shells, so `_IDSTACK` must be re-derived in each; the snippet is the single definition of that resolution order (`CLAUDE_PLUGIN_ROOT`, `IDSTACK_HOME`, then the Claude Code marketplace cache). `templates/manifest-schema.md` is spliced verbatim and so writes the resolution out longhand — smoke-test keeps the two in lockstep. +5. **`{{IDSTACK_RESOLVE}}`** placeholder (replaced by `templates/snippets/idstack-resolve.sh`). Unlike the other two, this one appears many times per template — once at the top of every bash block that calls `$_IDSTACK/bin/...`. Bash blocks run in separate shells, so `_IDSTACK` must be re-derived in each; the snippet is the single definition of that resolution order (`${CLAUDE_PLUGIN_ROOT}`, `IDSTACK_HOME`, then the Claude Code marketplace cache). Claude Code writes the plugin path into skill text only where it finds the exact token `${CLAUDE_PLUGIN_ROOT}`, and the Bash tool's shell never has the variable, so never give it a `:-` default. `templates/manifest-schema.md` is spliced verbatim and so writes the resolution out longhand — smoke-test keeps the two in lockstep and fails on any `${CLAUDE_PLUGIN_ROOT:-` in templates or skills. 6. **Timeline logging** (logs session data to `.idstack/timeline.jsonl` on completion) 7. **Writing standard check** — after each file that the skill writes for a person, a bash block runs `"$_IDSTACK/bin/idstack-ste-check"` on that file. smoke-test fails if a skill template has fewer check steps than it must have. A skill that starts a sub-agent puts a copy of the preamble's "Writing Standard (ASD-STE100)" section in the sub-agent prompt. diff --git a/skills/accessibility-review/SKILL.md b/skills/accessibility-review/SKILL.md index f906f1b..f54e2af 100644 --- a/skills/accessibility-review/SKILL.md +++ b/skills/accessibility-review/SKILL.md @@ -160,7 +160,8 @@ How to use the standard: 2. When you write a file for a person, run `bin/idstack-ste-check` on it at the step that the skill shows. 3. If the checker shows problems, write each sentence that it shows again. Then write the file again and run the checker again. 4. Run the checker a maximum of three times. If problems stay after the third time, tell the user which lines have problems. -5. If the checker does not run, continue the skill. Tell the user one time that the check did not run. +5. If the output is `STE_CHECK_UNAVAILABLE`, python3 is not available. Continue the skill. Tell the user one time that the check did not run. +6. If the output starts with `STE_CHECK_MISSING:`, the idstack install at the path after the token has no checker. Continue the skill. Tell the user one time that the check did not run. Give the user the path and tell the user to update idstack. If the path is empty, tell the user that idstack did not find its install. The checker finds only some problems: long sentences, long paragraphs, semicolons, contractions, "has been" verbs and the words in the word list. Apply all of the rules @@ -184,9 +185,10 @@ use them. ```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, then the Claude Code -# marketplace cache (highest version). Empty if none found; guard -# "$_IDSTACK/bin/..." calls accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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="" @@ -199,7 +201,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) @@ -224,7 +226,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done if [ -f ".idstack/project.json" ]; then @@ -304,7 +306,7 @@ 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 _dir in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do +for _dir in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_dir" ] && [ -d "$_dir" ]; then _IDSTACK="${_dir%/}"; break; fi done _CONSENSUS_KEY=$("$_IDSTACK/bin/idstack-consensus" status 2>/dev/null | grep -q '"api_key_configured": true' && echo "CONFIGURED" || echo "UNCONFIGURED") @@ -334,7 +336,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _HAS_TIMELINE=0 @@ -706,9 +708,9 @@ Generate an HTML report so the designer has a single document covering both comp ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -719,7 +721,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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. @@ -749,9 +753,9 @@ Write the HTML report at the path printed above (`.idstack/exports/ ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -762,10 +766,12 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. @@ -782,9 +788,9 @@ and atomically updates the top-level `updated` timestamp. The payload must inclu ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -795,7 +801,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-manifest-merge" --section accessibility_review --payload - <<'PAYLOAD' @@ -833,9 +841,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -846,7 +854,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-migrate" --init .idstack/project.json >/dev/null @@ -908,9 +918,9 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -921,7 +931,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done # Write a payload for your skill's section, then: @@ -1308,9 +1318,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1321,7 +1331,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"accessibility-review","event":"completed"}' @@ -1336,9 +1348,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1349,7 +1361,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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 af7f2d7..844c94e 100644 --- a/skills/accessibility-review/SKILL.md.tmpl +++ b/skills/accessibility-review/SKILL.md.tmpl @@ -321,7 +321,7 @@ Write the HTML report at the path printed above (`.idstack/exports/ ```bash {{IDSTACK_RESOLVE}} -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. diff --git a/skills/assessment-design/SKILL.md b/skills/assessment-design/SKILL.md index bb27e9f..ec1acfe 100644 --- a/skills/assessment-design/SKILL.md +++ b/skills/assessment-design/SKILL.md @@ -159,7 +159,8 @@ How to use the standard: 2. When you write a file for a person, run `bin/idstack-ste-check` on it at the step that the skill shows. 3. If the checker shows problems, write each sentence that it shows again. Then write the file again and run the checker again. 4. Run the checker a maximum of three times. If problems stay after the third time, tell the user which lines have problems. -5. If the checker does not run, continue the skill. Tell the user one time that the check did not run. +5. If the output is `STE_CHECK_UNAVAILABLE`, python3 is not available. Continue the skill. Tell the user one time that the check did not run. +6. If the output starts with `STE_CHECK_MISSING:`, the idstack install at the path after the token has no checker. Continue the skill. Tell the user one time that the check did not run. Give the user the path and tell the user to update idstack. If the path is empty, tell the user that idstack did not find its install. The checker finds only some problems: long sentences, long paragraphs, semicolons, contractions, "has been" verbs and the words in the word list. Apply all of the rules @@ -183,9 +184,10 @@ use them. ```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, then the Claude Code -# marketplace cache (highest version). Empty if none found; guard -# "$_IDSTACK/bin/..." calls accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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="" @@ -198,7 +200,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) @@ -223,7 +225,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done if [ -f ".idstack/project.json" ]; then @@ -303,7 +305,7 @@ 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 _dir in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do +for _dir in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_dir" ] && [ -d "$_dir" ]; then _IDSTACK="${_dir%/}"; break; fi done _CONSENSUS_KEY=$("$_IDSTACK/bin/idstack-consensus" status 2>/dev/null | grep -q '"api_key_configured": true' && echo "CONFIGURED" || echo "UNCONFIGURED") @@ -333,7 +335,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _HAS_TIMELINE=0 @@ -870,9 +872,9 @@ evidence tiers: ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -883,7 +885,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-consensus" verify --findings .idstack/.staged-findings.json --output .idstack/.verified-findings.json @@ -904,9 +908,9 @@ Before writing the manifest, generate an HTML report so the designer has a singl ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -917,7 +921,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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. @@ -949,9 +955,9 @@ In Mode 3 (audit-existing), every finding should reference the audited rubric cr ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -962,10 +968,12 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. @@ -988,9 +996,9 @@ The `assessments` payload must include `report_path` set to the value of `$_REPO ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1001,7 +1009,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done # Section 1: assessments @@ -1024,9 +1034,9 @@ If `.idstack/project.json` does not exist yet — the standalone case, where the ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1037,7 +1047,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-migrate" --init .idstack/project.json >/dev/null @@ -1157,9 +1169,9 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1170,7 +1182,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done # Write a payload for your skill's section, then: @@ -1557,9 +1569,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1570,7 +1582,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"assessment-design","event":"completed"}' @@ -1585,9 +1599,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1598,7 +1612,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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 f2f4e6f..2ec6875 100644 --- a/skills/assessment-design/SKILL.md.tmpl +++ b/skills/assessment-design/SKILL.md.tmpl @@ -504,7 +504,7 @@ In Mode 3 (audit-existing), every finding should reference the audited rubric cr ```bash {{IDSTACK_RESOLVE}} -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. diff --git a/skills/course-builder/SKILL.md b/skills/course-builder/SKILL.md index 3069c33..7d0a432 100644 --- a/skills/course-builder/SKILL.md +++ b/skills/course-builder/SKILL.md @@ -160,7 +160,8 @@ How to use the standard: 2. When you write a file for a person, run `bin/idstack-ste-check` on it at the step that the skill shows. 3. If the checker shows problems, write each sentence that it shows again. Then write the file again and run the checker again. 4. Run the checker a maximum of three times. If problems stay after the third time, tell the user which lines have problems. -5. If the checker does not run, continue the skill. Tell the user one time that the check did not run. +5. If the output is `STE_CHECK_UNAVAILABLE`, python3 is not available. Continue the skill. Tell the user one time that the check did not run. +6. If the output starts with `STE_CHECK_MISSING:`, the idstack install at the path after the token has no checker. Continue the skill. Tell the user one time that the check did not run. Give the user the path and tell the user to update idstack. If the path is empty, tell the user that idstack did not find its install. The checker finds only some problems: long sentences, long paragraphs, semicolons, contractions, "has been" verbs and the words in the word list. Apply all of the rules @@ -184,9 +185,10 @@ use them. ```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, then the Claude Code -# marketplace cache (highest version). Empty if none found; guard -# "$_IDSTACK/bin/..." calls accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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="" @@ -199,7 +201,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) @@ -224,7 +226,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done if [ -f ".idstack/project.json" ]; then @@ -304,7 +306,7 @@ 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 _dir in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do +for _dir in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_dir" ] && [ -d "$_dir" ]; then _IDSTACK="${_dir%/}"; break; fi done _CONSENSUS_KEY=$("$_IDSTACK/bin/idstack-consensus" status 2>/dev/null | grep -q '"api_key_configured": true' && echo "CONFIGURED" || echo "UNCONFIGURED") @@ -334,7 +336,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _HAS_TIMELINE=0 @@ -959,9 +961,9 @@ continue with remaining files. ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -972,10 +974,12 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ".idstack/course-content"; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ".idstack/course-content"; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. Show the user each sentence that you changed, before and after the change. @@ -987,9 +991,9 @@ Before updating the manifest, generate an HTML build report so the designer can ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1000,7 +1004,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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. @@ -1030,9 +1036,9 @@ Write the HTML report at the path printed above (`.idstack/exports/ ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1043,10 +1049,12 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. @@ -1058,9 +1066,9 @@ Save the `course_content` section to `.idstack/project.json` via `bin/idstack-ma ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1071,7 +1079,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-manifest-merge" --section course_content --payload - <<'PAYLOAD' @@ -1208,9 +1218,9 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1221,7 +1231,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done # Write a payload for your skill's section, then: @@ -1608,9 +1618,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1621,7 +1631,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"course-builder","event":"completed"}' @@ -1636,9 +1648,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1649,7 +1661,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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 6eb4d8c..dff92cd 100644 --- a/skills/course-builder/SKILL.md.tmpl +++ b/skills/course-builder/SKILL.md.tmpl @@ -548,7 +548,7 @@ continue with remaining files. ```bash {{IDSTACK_RESOLVE}} -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ".idstack/course-content"; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ".idstack/course-content"; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. Show the user each sentence that you changed, before and after the change. @@ -585,7 +585,7 @@ Write the HTML report at the path printed above (`.idstack/exports/ ```bash {{IDSTACK_RESOLVE}} -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. diff --git a/skills/course-export/SKILL.md b/skills/course-export/SKILL.md index 10389e7..66fe673 100644 --- a/skills/course-export/SKILL.md +++ b/skills/course-export/SKILL.md @@ -160,7 +160,8 @@ How to use the standard: 2. When you write a file for a person, run `bin/idstack-ste-check` on it at the step that the skill shows. 3. If the checker shows problems, write each sentence that it shows again. Then write the file again and run the checker again. 4. Run the checker a maximum of three times. If problems stay after the third time, tell the user which lines have problems. -5. If the checker does not run, continue the skill. Tell the user one time that the check did not run. +5. If the output is `STE_CHECK_UNAVAILABLE`, python3 is not available. Continue the skill. Tell the user one time that the check did not run. +6. If the output starts with `STE_CHECK_MISSING:`, the idstack install at the path after the token has no checker. Continue the skill. Tell the user one time that the check did not run. Give the user the path and tell the user to update idstack. If the path is empty, tell the user that idstack did not find its install. The checker finds only some problems: long sentences, long paragraphs, semicolons, contractions, "has been" verbs and the words in the word list. Apply all of the rules @@ -184,9 +185,10 @@ use them. ```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, then the Claude Code -# marketplace cache (highest version). Empty if none found; guard -# "$_IDSTACK/bin/..." calls accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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="" @@ -199,7 +201,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) @@ -224,7 +226,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done if [ -f ".idstack/project.json" ]; then @@ -304,7 +306,7 @@ 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 _dir in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do +for _dir in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_dir" ] && [ -d "$_dir" ]; then _IDSTACK="${_dir%/}"; break; fi done _CONSENSUS_KEY=$("$_IDSTACK/bin/idstack-consensus" status 2>/dev/null | grep -q '"api_key_configured": true' && echo "CONFIGURED" || echo "UNCONFIGURED") @@ -334,7 +336,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _HAS_TIMELINE=0 @@ -438,9 +440,9 @@ Before starting the export workflow, run the readiness dashboard: ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -451,7 +453,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-status" --readiness @@ -542,9 +546,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -555,7 +559,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done # Course-slug-based export folder. Required before any artifact write. @@ -582,9 +588,9 @@ If the course content is not in English, do not run this check. Go to "Export Fo ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -595,13 +601,15 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ".idstack/course-content"; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ".idstack/course-content"; fi ``` -1. If the output is `STE_CHECK_UNAVAILABLE`, tell the user one time that the check did not run. Continue the export. +1. If the output is `STE_CHECK_UNAVAILABLE` or starts with `STE_CHECK_MISSING:`, do step 5 or step 6 of "How to use the standard" in the preamble. Continue the export. 2. If the checker shows no problems, continue the export. 3. If the checker shows problems, show them to the user. Then use AskUserQuestion with two alternatives: "Continue the export" and "Stop the export to correct the content". 4. If the user selects "Stop the export to correct the content", stop this skill. Tell the user to correct the files in `.idstack/course-content/` and then run `/idstack:course-export` again. @@ -1351,9 +1359,9 @@ After the export completes (any path), write the HTML report at `$_REPORT_PATH` ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1364,10 +1372,12 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. @@ -1386,9 +1396,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1399,7 +1409,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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) @@ -1517,9 +1529,9 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1530,7 +1542,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done # Write a payload for your skill's section, then: @@ -1917,9 +1929,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1930,7 +1942,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"course-export","event":"completed"}' @@ -1945,9 +1959,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1958,7 +1972,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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 565b052..5d45ac4 100644 --- a/skills/course-export/SKILL.md.tmpl +++ b/skills/course-export/SKILL.md.tmpl @@ -137,10 +137,10 @@ If the course content is not in English, do not run this check. Go to "Export Fo ```bash {{IDSTACK_RESOLVE}} -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ".idstack/course-content"; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ".idstack/course-content"; fi ``` -1. If the output is `STE_CHECK_UNAVAILABLE`, tell the user one time that the check did not run. Continue the export. +1. If the output is `STE_CHECK_UNAVAILABLE` or starts with `STE_CHECK_MISSING:`, do step 5 or step 6 of "How to use the standard" in the preamble. Continue the export. 2. If the checker shows no problems, continue the export. 3. If the checker shows problems, show them to the user. Then use AskUserQuestion with two alternatives: "Continue the export" and "Stop the export to correct the content". 4. If the user selects "Stop the export to correct the content", stop this skill. Tell the user to correct the files in `.idstack/course-content/` and then run `/idstack:course-export` again. @@ -889,7 +889,7 @@ After the export completes (any path), write the HTML report at `$_REPORT_PATH` ```bash {{IDSTACK_RESOLVE}} -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. diff --git a/skills/course-import/SKILL.md b/skills/course-import/SKILL.md index 9768569..e28c9f2 100644 --- a/skills/course-import/SKILL.md +++ b/skills/course-import/SKILL.md @@ -160,7 +160,8 @@ How to use the standard: 2. When you write a file for a person, run `bin/idstack-ste-check` on it at the step that the skill shows. 3. If the checker shows problems, write each sentence that it shows again. Then write the file again and run the checker again. 4. Run the checker a maximum of three times. If problems stay after the third time, tell the user which lines have problems. -5. If the checker does not run, continue the skill. Tell the user one time that the check did not run. +5. If the output is `STE_CHECK_UNAVAILABLE`, python3 is not available. Continue the skill. Tell the user one time that the check did not run. +6. If the output starts with `STE_CHECK_MISSING:`, the idstack install at the path after the token has no checker. Continue the skill. Tell the user one time that the check did not run. Give the user the path and tell the user to update idstack. If the path is empty, tell the user that idstack did not find its install. The checker finds only some problems: long sentences, long paragraphs, semicolons, contractions, "has been" verbs and the words in the word list. Apply all of the rules @@ -184,9 +185,10 @@ use them. ```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, then the Claude Code -# marketplace cache (highest version). Empty if none found; guard -# "$_IDSTACK/bin/..." calls accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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="" @@ -199,7 +201,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) @@ -224,7 +226,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done if [ -f ".idstack/project.json" ]; then @@ -304,7 +306,7 @@ 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 _dir in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do +for _dir in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_dir" ] && [ -d "$_dir" ]; then _IDSTACK="${_dir%/}"; break; fi done _CONSENSUS_KEY=$("$_IDSTACK/bin/idstack-consensus" status 2>/dev/null | grep -q '"api_key_configured": true' && echo "CONFIGURED" || echo "UNCONFIGURED") @@ -334,7 +336,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _HAS_TIMELINE=0 @@ -1123,9 +1125,9 @@ Before writing the manifest, generate an HTML report so the designer has a singl ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1136,7 +1138,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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. @@ -1170,9 +1174,9 @@ Write the HTML report at the path printed above (`.idstack/exports/ ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1183,10 +1187,12 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. @@ -1301,9 +1307,9 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1314,7 +1320,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done # Write a payload for your skill's section, then: @@ -1701,9 +1707,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1714,7 +1720,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"course-import","event":"completed"}' @@ -1729,9 +1737,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1742,7 +1750,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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 6fb9087..182f600 100644 --- a/skills/course-import/SKILL.md.tmpl +++ b/skills/course-import/SKILL.md.tmpl @@ -742,7 +742,7 @@ Write the HTML report at the path printed above (`.idstack/exports/ ```bash {{IDSTACK_RESOLVE}} -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. diff --git a/skills/course-quality-review/SKILL.md b/skills/course-quality-review/SKILL.md index 36cb7c3..585c83d 100644 --- a/skills/course-quality-review/SKILL.md +++ b/skills/course-quality-review/SKILL.md @@ -160,7 +160,8 @@ How to use the standard: 2. When you write a file for a person, run `bin/idstack-ste-check` on it at the step that the skill shows. 3. If the checker shows problems, write each sentence that it shows again. Then write the file again and run the checker again. 4. Run the checker a maximum of three times. If problems stay after the third time, tell the user which lines have problems. -5. If the checker does not run, continue the skill. Tell the user one time that the check did not run. +5. If the output is `STE_CHECK_UNAVAILABLE`, python3 is not available. Continue the skill. Tell the user one time that the check did not run. +6. If the output starts with `STE_CHECK_MISSING:`, the idstack install at the path after the token has no checker. Continue the skill. Tell the user one time that the check did not run. Give the user the path and tell the user to update idstack. If the path is empty, tell the user that idstack did not find its install. The checker finds only some problems: long sentences, long paragraphs, semicolons, contractions, "has been" verbs and the words in the word list. Apply all of the rules @@ -184,9 +185,10 @@ use them. ```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, then the Claude Code -# marketplace cache (highest version). Empty if none found; guard -# "$_IDSTACK/bin/..." calls accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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="" @@ -199,7 +201,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) @@ -224,7 +226,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done if [ -f ".idstack/project.json" ]; then @@ -304,7 +306,7 @@ 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 _dir in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do +for _dir in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_dir" ] && [ -d "$_dir" ]; then _IDSTACK="${_dir%/}"; break; fi done _CONSENSUS_KEY=$("$_IDSTACK/bin/idstack-consensus" status 2>/dev/null | grep -q '"api_key_configured": true' && echo "CONFIGURED" || echo "UNCONFIGURED") @@ -334,7 +336,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _HAS_TIMELINE=0 @@ -1094,9 +1096,9 @@ or dual-coding), and calibrates evidence tiers: ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1107,7 +1109,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-consensus" verify --findings .idstack/.staged-findings.json --output .idstack/.verified-findings.json @@ -1130,9 +1134,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1143,7 +1147,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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) @@ -1244,9 +1250,9 @@ After writing the manifest, generate a shareable HTML report. The report follows ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1257,7 +1263,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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. @@ -1292,9 +1300,9 @@ Write the HTML report at the path printed above (`.idstack/exports/ ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1305,10 +1313,12 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. @@ -1357,9 +1367,9 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1370,7 +1380,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done # Write a payload for your skill's section, then: @@ -1759,9 +1769,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1772,7 +1782,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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}}' @@ -1787,9 +1799,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1800,7 +1812,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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 da0cd36..101db21 100644 --- a/skills/course-quality-review/SKILL.md.tmpl +++ b/skills/course-quality-review/SKILL.md.tmpl @@ -830,7 +830,7 @@ Write the HTML report at the path printed above (`.idstack/exports/ ```bash {{IDSTACK_RESOLVE}} -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. diff --git a/skills/learn/SKILL.md b/skills/learn/SKILL.md index 630249a..f80149b 100644 --- a/skills/learn/SKILL.md +++ b/skills/learn/SKILL.md @@ -155,7 +155,8 @@ How to use the standard: 2. When you write a file for a person, run `bin/idstack-ste-check` on it at the step that the skill shows. 3. If the checker shows problems, write each sentence that it shows again. Then write the file again and run the checker again. 4. Run the checker a maximum of three times. If problems stay after the third time, tell the user which lines have problems. -5. If the checker does not run, continue the skill. Tell the user one time that the check did not run. +5. If the output is `STE_CHECK_UNAVAILABLE`, python3 is not available. Continue the skill. Tell the user one time that the check did not run. +6. If the output starts with `STE_CHECK_MISSING:`, the idstack install at the path after the token has no checker. Continue the skill. Tell the user one time that the check did not run. Give the user the path and tell the user to update idstack. If the path is empty, tell the user that idstack did not find its install. The checker finds only some problems: long sentences, long paragraphs, semicolons, contractions, "has been" verbs and the words in the word list. Apply all of the rules @@ -179,9 +180,10 @@ use them. ```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, then the Claude Code -# marketplace cache (highest version). Empty if none found; guard -# "$_IDSTACK/bin/..." calls accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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="" @@ -194,7 +196,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) @@ -219,7 +221,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done if [ -f ".idstack/project.json" ]; then @@ -299,7 +301,7 @@ 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 _dir in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do +for _dir in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_dir" ] && [ -d "$_dir" ]; then _IDSTACK="${_dir%/}"; break; fi done _CONSENSUS_KEY=$("$_IDSTACK/bin/idstack-consensus" status 2>/dev/null | grep -q '"api_key_configured": true' && echo "CONFIGURED" || echo "UNCONFIGURED") @@ -329,7 +331,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _HAS_TIMELINE=0 @@ -443,9 +445,9 @@ this is the default. ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -456,7 +458,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-learnings-search" --limit 10 @@ -478,9 +482,9 @@ Search learnings by keyword. Supports `--cross-project` to include global learni ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -491,7 +495,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-learnings-search" --keyword KEYWORD --limit 10 @@ -501,9 +507,9 @@ For cross-project search: ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -514,7 +520,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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 @@ -529,9 +537,9 @@ Delete a learning by its key. Always confirm with the user before deleting. ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -542,7 +550,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-learnings-delete" KEY @@ -555,9 +565,9 @@ Copy a local learning to the global store so it's available across projects. ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -568,7 +578,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-learnings-promote" KEY @@ -581,9 +593,9 @@ Export all learnings to a markdown file. ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -594,7 +606,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-learnings-search" --limit 1000 @@ -608,9 +622,9 @@ and write to `.idstack/learnings-export.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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -621,13 +635,15 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ".idstack/learnings-export.md"; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ".idstack/learnings-export.md"; fi ``` -If the output is `STE_CHECK_UNAVAILABLE`, tell the user one time that the check did not run. If the checker shows problems, show them to the user. Do not change the export file. +If the output is `STE_CHECK_UNAVAILABLE` or starts with `STE_CHECK_MISSING:`, do step 5 or step 6 of "How to use the standard" in the preamble. If the checker shows problems, show them to the user. Do not change the export file. ## Workflow diff --git a/skills/learn/SKILL.md.tmpl b/skills/learn/SKILL.md.tmpl index fbe44e8..1978376 100644 --- a/skills/learn/SKILL.md.tmpl +++ b/skills/learn/SKILL.md.tmpl @@ -95,10 +95,10 @@ and write to `.idstack/learnings-export.md`. ```bash {{IDSTACK_RESOLVE}} -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ".idstack/learnings-export.md"; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ".idstack/learnings-export.md"; fi ``` -If the output is `STE_CHECK_UNAVAILABLE`, tell the user one time that the check did not run. If the checker shows problems, show them to the user. Do not change the export file. +If the output is `STE_CHECK_UNAVAILABLE` or starts with `STE_CHECK_MISSING:`, do step 5 or step 6 of "How to use the standard" in the preamble. If the checker shows problems, show them to the user. Do not change the export file. ## Workflow diff --git a/skills/learning-objectives/SKILL.md b/skills/learning-objectives/SKILL.md index a3787c3..d4e338c 100644 --- a/skills/learning-objectives/SKILL.md +++ b/skills/learning-objectives/SKILL.md @@ -159,7 +159,8 @@ How to use the standard: 2. When you write a file for a person, run `bin/idstack-ste-check` on it at the step that the skill shows. 3. If the checker shows problems, write each sentence that it shows again. Then write the file again and run the checker again. 4. Run the checker a maximum of three times. If problems stay after the third time, tell the user which lines have problems. -5. If the checker does not run, continue the skill. Tell the user one time that the check did not run. +5. If the output is `STE_CHECK_UNAVAILABLE`, python3 is not available. Continue the skill. Tell the user one time that the check did not run. +6. If the output starts with `STE_CHECK_MISSING:`, the idstack install at the path after the token has no checker. Continue the skill. Tell the user one time that the check did not run. Give the user the path and tell the user to update idstack. If the path is empty, tell the user that idstack did not find its install. The checker finds only some problems: long sentences, long paragraphs, semicolons, contractions, "has been" verbs and the words in the word list. Apply all of the rules @@ -183,9 +184,10 @@ use them. ```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, then the Claude Code -# marketplace cache (highest version). Empty if none found; guard -# "$_IDSTACK/bin/..." calls accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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="" @@ -198,7 +200,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) @@ -223,7 +225,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done if [ -f ".idstack/project.json" ]; then @@ -303,7 +305,7 @@ 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 _dir in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do +for _dir in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_dir" ] && [ -d "$_dir" ]; then _IDSTACK="${_dir%/}"; break; fi done _CONSENSUS_KEY=$("$_IDSTACK/bin/idstack-consensus" status 2>/dev/null | grep -q '"api_key_configured": true' && echo "CONFIGURED" || echo "UNCONFIGURED") @@ -333,7 +335,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _HAS_TIMELINE=0 @@ -729,9 +731,9 @@ Before writing the manifest, generate an HTML report so the designer has a singl ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -742,7 +744,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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. @@ -773,9 +777,9 @@ Every finding in the HTML must correspond to an entry in `learning_objectives.al ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -786,10 +790,12 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. @@ -808,9 +814,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -821,7 +827,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-manifest-merge" --section learning_objectives --payload - <<'PAYLOAD' @@ -909,9 +917,9 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -922,7 +930,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done # Write a payload for your skill's section, then: @@ -1309,9 +1317,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1322,7 +1330,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"learning-objectives","event":"completed"}' @@ -1337,9 +1347,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1350,7 +1360,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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 10bf398..38a9075 100644 --- a/skills/learning-objectives/SKILL.md.tmpl +++ b/skills/learning-objectives/SKILL.md.tmpl @@ -345,7 +345,7 @@ Every finding in the HTML must correspond to an entry in `learning_objectives.al ```bash {{IDSTACK_RESOLVE}} -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. diff --git a/skills/needs-analysis/SKILL.md b/skills/needs-analysis/SKILL.md index 4f28180..6aeeca8 100644 --- a/skills/needs-analysis/SKILL.md +++ b/skills/needs-analysis/SKILL.md @@ -159,7 +159,8 @@ How to use the standard: 2. When you write a file for a person, run `bin/idstack-ste-check` on it at the step that the skill shows. 3. If the checker shows problems, write each sentence that it shows again. Then write the file again and run the checker again. 4. Run the checker a maximum of three times. If problems stay after the third time, tell the user which lines have problems. -5. If the checker does not run, continue the skill. Tell the user one time that the check did not run. +5. If the output is `STE_CHECK_UNAVAILABLE`, python3 is not available. Continue the skill. Tell the user one time that the check did not run. +6. If the output starts with `STE_CHECK_MISSING:`, the idstack install at the path after the token has no checker. Continue the skill. Tell the user one time that the check did not run. Give the user the path and tell the user to update idstack. If the path is empty, tell the user that idstack did not find its install. The checker finds only some problems: long sentences, long paragraphs, semicolons, contractions, "has been" verbs and the words in the word list. Apply all of the rules @@ -183,9 +184,10 @@ use them. ```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, then the Claude Code -# marketplace cache (highest version). Empty if none found; guard -# "$_IDSTACK/bin/..." calls accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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="" @@ -198,7 +200,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) @@ -223,7 +225,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done if [ -f ".idstack/project.json" ]; then @@ -303,7 +305,7 @@ 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 _dir in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do +for _dir in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_dir" ] && [ -d "$_dir" ]; then _IDSTACK="${_dir%/}"; break; fi done _CONSENSUS_KEY=$("$_IDSTACK/bin/idstack-consensus" status 2>/dev/null | grep -q '"api_key_configured": true' && echo "CONFIGURED" || echo "UNCONFIGURED") @@ -333,7 +335,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _HAS_TIMELINE=0 @@ -731,9 +733,9 @@ Generate an HTML report so the designer has a single document to read. The repor ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -744,7 +746,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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. @@ -778,9 +782,9 @@ Every finding in the HTML must correspond to a finding the manifest's `needs_ana ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -791,10 +795,12 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. @@ -859,9 +865,9 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -872,7 +878,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done # Write a payload for your skill's section, then: @@ -1259,9 +1265,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1272,7 +1278,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"needs-analysis","event":"completed"}' @@ -1287,9 +1295,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1300,7 +1308,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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 31f70d1..23b200a 100644 --- a/skills/needs-analysis/SKILL.md.tmpl +++ b/skills/needs-analysis/SKILL.md.tmpl @@ -350,7 +350,7 @@ Every finding in the HTML must correspond to a finding the manifest's `needs_ana ```bash {{IDSTACK_RESOLVE}} -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. diff --git a/skills/pipeline/SKILL.md b/skills/pipeline/SKILL.md index 09b84d0..ee92c2e 100644 --- a/skills/pipeline/SKILL.md +++ b/skills/pipeline/SKILL.md @@ -159,7 +159,8 @@ How to use the standard: 2. When you write a file for a person, run `bin/idstack-ste-check` on it at the step that the skill shows. 3. If the checker shows problems, write each sentence that it shows again. Then write the file again and run the checker again. 4. Run the checker a maximum of three times. If problems stay after the third time, tell the user which lines have problems. -5. If the checker does not run, continue the skill. Tell the user one time that the check did not run. +5. If the output is `STE_CHECK_UNAVAILABLE`, python3 is not available. Continue the skill. Tell the user one time that the check did not run. +6. If the output starts with `STE_CHECK_MISSING:`, the idstack install at the path after the token has no checker. Continue the skill. Tell the user one time that the check did not run. Give the user the path and tell the user to update idstack. If the path is empty, tell the user that idstack did not find its install. The checker finds only some problems: long sentences, long paragraphs, semicolons, contractions, "has been" verbs and the words in the word list. Apply all of the rules @@ -183,9 +184,10 @@ use them. ```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, then the Claude Code -# marketplace cache (highest version). Empty if none found; guard -# "$_IDSTACK/bin/..." calls accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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="" @@ -198,7 +200,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) @@ -223,7 +225,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done if [ -f ".idstack/project.json" ]; then @@ -303,7 +305,7 @@ 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 _dir in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do +for _dir in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_dir" ] && [ -d "$_dir" ]; then _IDSTACK="${_dir%/}"; break; fi done _CONSENSUS_KEY=$("$_IDSTACK/bin/idstack-consensus" status 2>/dev/null | grep -q '"api_key_configured": true' && echo "CONFIGURED" || echo "UNCONFIGURED") @@ -333,7 +335,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _HAS_TIMELINE=0 @@ -560,9 +562,9 @@ After each skill completes (or after the orchestrator finishes the run, includin ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -573,7 +575,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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 "") @@ -619,9 +623,9 @@ The dashboard is overwritten on every pipeline run — historical dashboards are ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -632,10 +636,12 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. @@ -652,9 +658,9 @@ When all remaining skills have been executed: ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -665,7 +671,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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"}' diff --git a/skills/pipeline/SKILL.md.tmpl b/skills/pipeline/SKILL.md.tmpl index 6f3a85f..c816263 100644 --- a/skills/pipeline/SKILL.md.tmpl +++ b/skills/pipeline/SKILL.md.tmpl @@ -191,7 +191,7 @@ The dashboard is overwritten on every pipeline run — historical dashboards are ```bash {{IDSTACK_RESOLVE}} -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Do the check a maximum of three times. diff --git a/skills/red-team/SKILL.md b/skills/red-team/SKILL.md index 675f4db..d4bff6f 100644 --- a/skills/red-team/SKILL.md +++ b/skills/red-team/SKILL.md @@ -162,7 +162,8 @@ How to use the standard: 2. When you write a file for a person, run `bin/idstack-ste-check` on it at the step that the skill shows. 3. If the checker shows problems, write each sentence that it shows again. Then write the file again and run the checker again. 4. Run the checker a maximum of three times. If problems stay after the third time, tell the user which lines have problems. -5. If the checker does not run, continue the skill. Tell the user one time that the check did not run. +5. If the output is `STE_CHECK_UNAVAILABLE`, python3 is not available. Continue the skill. Tell the user one time that the check did not run. +6. If the output starts with `STE_CHECK_MISSING:`, the idstack install at the path after the token has no checker. Continue the skill. Tell the user one time that the check did not run. Give the user the path and tell the user to update idstack. If the path is empty, tell the user that idstack did not find its install. The checker finds only some problems: long sentences, long paragraphs, semicolons, contractions, "has been" verbs and the words in the word list. Apply all of the rules @@ -186,9 +187,10 @@ use them. ```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, then the Claude Code -# marketplace cache (highest version). Empty if none found; guard -# "$_IDSTACK/bin/..." calls accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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="" @@ -201,7 +203,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) @@ -226,7 +228,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done if [ -f ".idstack/project.json" ]; then @@ -306,7 +308,7 @@ 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 _dir in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do +for _dir in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_dir" ] && [ -d "$_dir" ]; then _IDSTACK="${_dir%/}"; break; fi done _CONSENSUS_KEY=$("$_IDSTACK/bin/idstack-consensus" status 2>/dev/null | grep -q '"api_key_configured": true' && echo "CONFIGURED" || echo "UNCONFIGURED") @@ -336,7 +338,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _HAS_TIMELINE=0 @@ -726,9 +728,9 @@ presentation or dual-coding), and calibrates evidence tiers: ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -739,7 +741,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-consensus" verify --findings .idstack/.staged-findings.json --output .idstack/.verified-findings.json @@ -761,9 +765,9 @@ The orchestrator emits an HTML report. Follow the **visual contract** in `templa ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -774,7 +778,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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. @@ -806,9 +812,9 @@ Each finding **must** have a stable id of the form `-` (e.g., `ali ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -819,10 +825,12 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section at the end of this prompt. Do the check a maximum of three times. If these steps tell you to tell the user something, put it in your executive summary. @@ -878,9 +886,9 @@ If the user pushes back on any specific fix, mark it deferred and continue. ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -891,10 +899,12 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Change only the sentences that you wrote or changed in this step. Do not change the other text in the file. Do the check a maximum of three times. If the checker shows problems only in sentences that you did not write or change, the check of that file is complete. Tell the user one time that these lines had problems before your changes. @@ -910,9 +920,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -923,7 +933,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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' @@ -963,9 +975,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -976,7 +988,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-migrate" --init .idstack/project.json >/dev/null @@ -1007,9 +1021,9 @@ 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1020,7 +1034,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done "$_IDSTACK/bin/idstack-timeline-log" '{"skill":"red-team","event":"completed"}' @@ -1033,9 +1049,9 @@ If you discover a non-obvious project-specific quirk during this session (LMS be ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -1046,7 +1062,9 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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 eca0aed..35aada0 100644 --- a/skills/red-team/SKILL.md.tmpl +++ b/skills/red-team/SKILL.md.tmpl @@ -361,7 +361,7 @@ Each finding **must** have a stable id of the form `-` (e.g., `ali ```bash {{IDSTACK_RESOLVE}} -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section at the end of this prompt. Do the check a maximum of three times. If these steps tell you to tell the user something, put it in your executive summary. @@ -416,7 +416,7 @@ If the user pushes back on any specific fix, mark it deferred and continue. ```bash {{IDSTACK_RESOLVE}} -if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ""; else echo "STE_CHECK_UNAVAILABLE"; fi +if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; else "$_IDSTACK/bin/idstack-ste-check" ""; fi ``` For the result, do the steps in "How to use the standard" in the "Writing Standard (ASD-STE100)" section of the preamble. Change only the sentences that you wrote or changed in this step. Do not change the other text in the file. Do the check a maximum of three times. If the checker shows problems only in sentences that you did not write or change, the check of that file is complete. Tell the user one time that these lines had problems before your changes. diff --git a/templates/manifest-schema.md b/templates/manifest-schema.md index 31af0f7..e04aaa9 100644 --- a/templates/manifest-schema.md +++ b/templates/manifest-schema.md @@ -24,9 +24,9 @@ The HTML report follows the visual contract in `templates/report.html.tmpl` and ```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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -37,7 +37,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done # Write a payload for your skill's section, then: diff --git a/templates/preamble.md b/templates/preamble.md index a67d5ed..50e7194 100644 --- a/templates/preamble.md +++ b/templates/preamble.md @@ -139,7 +139,8 @@ How to use the standard: 2. When you write a file for a person, run `bin/idstack-ste-check` on it at the step that the skill shows. 3. If the checker shows problems, write each sentence that it shows again. Then write the file again and run the checker again. 4. Run the checker a maximum of three times. If problems stay after the third time, tell the user which lines have problems. -5. If the checker does not run, continue the skill. Tell the user one time that the check did not run. +5. If the output is `STE_CHECK_UNAVAILABLE`, python3 is not available. Continue the skill. Tell the user one time that the check did not run. +6. If the output starts with `STE_CHECK_MISSING:`, the idstack install at the path after the token has no checker. Continue the skill. Tell the user one time that the check did not run. Give the user the path and tell the user to update idstack. If the path is empty, tell the user that idstack did not find its install. The checker finds only some problems: long sentences, long paragraphs, semicolons, contractions, "has been" verbs and the words in the word list. Apply all of the rules @@ -163,9 +164,10 @@ use them. ```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, then the Claude Code -# marketplace cache (highest version). Empty if none found; guard -# "$_IDSTACK/bin/..." calls accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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="" @@ -178,7 +180,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _UPD=$("$_IDSTACK/bin/idstack-update-check" 2>/dev/null || true) @@ -203,7 +205,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done if [ -f ".idstack/project.json" ]; then @@ -283,7 +285,7 @@ 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 _dir in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do +for _dir in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_dir" ] && [ -d "$_dir" ]; then _IDSTACK="${_dir%/}"; break; fi done _CONSENSUS_KEY=$("$_IDSTACK/bin/idstack-consensus" status 2>/dev/null | grep -q '"api_key_configured": true' && echo "CONFIGURED" || echo "UNCONFIGURED") @@ -313,7 +315,7 @@ 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:-}" "$_idstack_cache"; do +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done _HAS_TIMELINE=0 diff --git a/templates/snippets/idstack-resolve.sh b/templates/snippets/idstack-resolve.sh index ed8dce4..44c7ba6 100644 --- a/templates/snippets/idstack-resolve.sh +++ b/templates/snippets/idstack-resolve.sh @@ -1,8 +1,8 @@ # 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, then the Claude Code -# marketplace cache. Empty if none found; guard "$_IDSTACK/bin/..." calls -# accordingly. +# available here. Priority: the plugin root that Claude Code writes into the +# skill text, then IDSTACK_HOME, 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 @@ -13,6 +13,8 @@ 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:-}" "$_idstack_cache"; do +# CLAUDE_PLUGIN_ROOT has no ":-" default on purpose. Claude Code replaces only +# the exact braced token in skill text. The Bash tool's shell does not have it. +for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do if [ -n "$_p" ] && [ -d "$_p" ]; then _IDSTACK="${_p%/}"; break; fi done diff --git a/test/mutation-test.sh b/test/mutation-test.sh index d37549a..da18b90 100755 --- a/test/mutation-test.sh +++ b/test/mutation-test.sh @@ -1620,9 +1620,11 @@ old = ('**Writing standard check.** Run this command on the export file. This ch 'problems. Do not change the learnings in the export. Other sessions wrote them, many ' 'before the writing standard.\n\n' '```bash\n{{IDSTACK_RESOLVE}}\n' - 'if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ' - '".idstack/learnings-export.md"; else echo "STE_CHECK_UNAVAILABLE"; fi\n```\n\n' - 'If the output is `STE_CHECK_UNAVAILABLE`, tell the user one time that the check did not run. ' + 'if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; ' + 'elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; ' + 'else "$_IDSTACK/bin/idstack-ste-check" ".idstack/learnings-export.md"; fi\n```\n\n' + 'If the output is `STE_CHECK_UNAVAILABLE` or starts with `STE_CHECK_MISSING:`, do step 5 or step 6 ' + 'of "How to use the standard" in the preamble. ' 'If the checker shows problems, show them to the user. Do not change the export file.\n\n') assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) s = s.replace(old, '', 1) @@ -1651,8 +1653,9 @@ python3 - "$WORK/r/skills/course-builder/SKILL.md.tmpl" <<'PY' import sys p = sys.argv[1]; s = open(p, encoding='utf-8').read() old = ('```bash\n{{IDSTACK_RESOLVE}}\n' - 'if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; then "$_IDSTACK/bin/idstack-ste-check" ' - '".idstack/course-content"; else echo "STE_CHECK_UNAVAILABLE"; fi\n```\n') + 'if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; ' + 'elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; ' + 'else "$_IDSTACK/bin/idstack-ste-check" ".idstack/course-content"; fi\n```\n') assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) s = s.replace(old, '', 1) open(p, 'w', encoding='utf-8').write(s) @@ -1855,6 +1858,62 @@ open(p, 'w', encoding='utf-8').write(s) PY expect_fail "the rendered landing test ignores a Chrome that stops at startup" "$WORK/r/test/smoke-test.sh" "$WORK/r" +# 51a-51c. Claude Code writes the plugin root into skill text only where it finds +# the exact token ${CLAUDE_PLUGIN_ROOT}. The Bash tool's shell does not have the +# variable. With a ":-" default the chain fell through to an old cache install that +# had no checker, and the check step said only that the check did not run. +# 51a. The chain gives the plugin root a ":-" default again in all five copies -> +# smoke-test must fail. All copies move together, so the x3 lockstep count passes +# and only the substitution checks can see it. +fresh +python3 - "$WORK/r" <<'PY' +import sys +r = sys.argv[1] +for rel, n in (("templates/snippets/idstack-resolve.sh", 1), ("templates/preamble.md", 4), + ("templates/manifest-schema.md", 1)): + p = r + "/" + rel; s = open(p, encoding='utf-8').read() + old = '"${CLAUDE_PLUGIN_ROOT}"' + assert s.count(old) == n, '%s: anchor count %d' % (rel, s.count(old)) + s = s.replace(old, '"${CLAUDE_PLUGIN_ROOT:-}"') + open(p, 'w', encoding='utf-8').write(s) +PY +regen +expect_fail "the resolve chain gives the plugin root a default again" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 51b. Only the longhand copy in manifest-schema.md gets the ":-" default again -> +# smoke-test must fail. The x3 count and the substitution run read only the +# preamble's copies, so only the repo-wide grep sees this copy. +fresh +python3 - "$WORK/r/templates/manifest-schema.md" <<'PY' +import sys +p = sys.argv[1]; s = open(p, encoding='utf-8').read() +old = 'for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do' +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, 'for _p in "${CLAUDE_PLUGIN_ROOT:-}" "${IDSTACK_HOME:-}" "$_idstack_cache"; do', 1) +open(p, 'w', encoding='utf-8').write(s) +PY +regen +expect_fail "the manifest-schema resolve chain gives the plugin root a default" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 51c. learn's check step folds a missing checker into the python3 token again -> +# smoke-test must fail. The step still calls the checker, so the count of check +# steps passes, and only the missing-checker run sees it. +fresh +python3 - "$WORK/r/skills/learn/SKILL.md.tmpl" <<'PY' +import sys +p = sys.argv[1]; s = open(p, encoding='utf-8').read() +old = ('if [ ! -x "$_IDSTACK/bin/idstack-ste-check" ]; then echo "STE_CHECK_MISSING: $_IDSTACK"; ' + 'elif ! command -v python3 >/dev/null 2>&1; then echo "STE_CHECK_UNAVAILABLE"; ' + 'else "$_IDSTACK/bin/idstack-ste-check" ".idstack/learnings-export.md"; fi') +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, 'if [ -x "$_IDSTACK/bin/idstack-ste-check" ] && command -v python3 >/dev/null 2>&1; ' + 'then "$_IDSTACK/bin/idstack-ste-check" ".idstack/learnings-export.md"; ' + 'else echo "STE_CHECK_UNAVAILABLE"; fi', 1) +open(p, 'w', encoding='utf-8').write(s) +PY +regen +expect_fail "a check step folds a missing checker into the python3 token" "$WORK/r/test/smoke-test.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 77778a9..f2893fd 100755 --- a/test/smoke-test.sh +++ b/test/smoke-test.sh @@ -237,6 +237,23 @@ for skill in $SKILLS; do 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'" +# Claude Code writes the plugin root into a skill's text only where it finds the +# exact token ${CLAUDE_PLUGIN_ROOT}. The Bash tool's shell does not have the +# variable, and a token with a default (":-") stays as it is. The chain used +# that form until this check, so every skill fell through to the newest cache +# dir: under --plugin-dir, and for a marketplace added from a local path, that +# was an old install with no bin/idstack-ste-check. Do to the generated text +# what Claude Code does, then run the first chain with a stale cache present. +RESOLVE_SIM=$(mktemp -d) +mkdir -p "$RESOLVE_SIM/plugin" "$RESOLVE_SIM/home/.claude/plugins/cache/idstack/idstack/0.0.1.0" +RESOLVE_SIM_GOT=$({ awk '/^_IDSTACK=""$/{f=1} f{print} f && /^done$/{exit}' "$IDSTACK_DIR/skills/learn/SKILL.md" \ + | sed "s|\${CLAUDE_PLUGIN_ROOT}|$RESOLVE_SIM/plugin|g"; echo 'printf "%s" "$_IDSTACK"'; } \ + | env -u CLAUDE_PLUGIN_ROOT -u IDSTACK_HOME HOME="$RESOLVE_SIM/home" bash) +check "the substituted plugin root wins over a stale cache install" "[ '$RESOLVE_SIM_GOT' = '$RESOLVE_SIM/plugin' ]" +rm -rf "$RESOLVE_SIM" +# The x3 count above pins only the preamble's "for _p in" copies. The consensus +# block ("for _dir in") and the longhand copy in manifest-schema.md need this. +check "no resolve chain gives the plugin root a default" "! grep -rlF '\${CLAUDE_PLUGIN_ROOT:-' '$IDSTACK_DIR/templates' $IDSTACK_DIR/skills/*/SKILL.md.tmpl $IDSTACK_DIR/skills/*/SKILL.md" # Pipeline orchestrator must produce index.html under the course folder. # Use -E (extended regex) so the `|` alternation works under BSD grep too; @@ -338,6 +355,16 @@ for skill in $SKILLS; do got=$(grep -cF '"$_IDSTACK/bin/idstack-ste-check"' "$IDSTACK_DIR/skills/$skill/SKILL.md.tmpl" || true) check "$skill SKILL.md.tmpl runs the ste checker at all $want check steps" "[ '$got' -eq $want ]" done +# A check step must tell an install with no checker apart from a missing +# python3. Under --plugin-dir the chain once found a 3.5.1.0 install with no +# checker, and the step printed the python3 token. The skill then said only +# "the check did not run", and nothing told the user that the install was old. +STE_NOCHK=$(mktemp -d) +for skill in $SKILLS; do + check "$skill check steps name an install that has no checker" \ + "grep -F '\"\$_IDSTACK/bin/idstack-ste-check\"' '$IDSTACK_DIR/skills/$skill/SKILL.md.tmpl' | while IFS= read -r l; do _IDSTACK='$STE_NOCHK' bash -c \"\$l\" | grep -qxF 'STE_CHECK_MISSING: $STE_NOCHK' || exit 1; done" +done +rm -rf "$STE_NOCHK" # Every report and the course dashboard copy the fixed labels in these two # templates, so one bad label puts a problem in every report. if command -v python3 &>/dev/null; then From f0611347b12c397b3df9b6f83b914be0007378dd Mon Sep 17 00:00:00 2001 From: Philippos Savvides Date: Wed, 7 Oct 2026 17:06:50 -0700 Subject: [PATCH 4/7] fix: name the report folder from the course title on a first run needs-analysis and course-import read project_name from the manifest before they write it, so a first run put the report in exports/untitled-course/. Take the slug from the session's course title, the same text the manifest step writes. Mutations 52a-52b guard the fix. Co-Authored-By: Claude Opus 5.5 --- skills/course-import/SKILL.md | 12 ++++++++--- skills/course-import/SKILL.md.tmpl | 12 ++++++++--- skills/needs-analysis/SKILL.md | 12 +++++++++-- skills/needs-analysis/SKILL.md.tmpl | 12 +++++++++-- test/mutation-test.sh | 33 +++++++++++++++++++++++++++++ test/smoke-test.sh | 14 ++++++++++++ 6 files changed, 85 insertions(+), 10 deletions(-) diff --git a/skills/course-import/SKILL.md b/skills/course-import/SKILL.md index e28c9f2..042f99e 100644 --- a/skills/course-import/SKILL.md +++ b/skills/course-import/SKILL.md @@ -1122,6 +1122,8 @@ to check the Bloom's levels and the 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`. +In this command, replace `` with the course title from the import. Then run the command. + ```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 @@ -1143,8 +1145,12 @@ fi for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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 "") +# Compute the course slug from this session's course title and prepare the export folder. +# Do not read project_name from the manifest here. Step 6 writes it, so on a first import +# the manifest has no name yet and the report lands in exports/untitled-course/. +IFS= read -r _PROJECT_NAME <<'IDSTACK_PROJECT_NAME' + +IDSTACK_PROJECT_NAME _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") _EXPORT_DIR=".idstack/exports/$_SLUG" _REPORT_PATH="$_EXPORT_DIR/course-import.html" @@ -1218,7 +1224,7 @@ Create or update the project manifest. **Fields populated by /idstack:course-import:** -- `project_name` — from course title +- `project_name` — from course title. Use the text that you put in place of `` in Step 5. - `context.modality` — inferred from course structure (async=online, sync sessions=hybrid) - `context.timeline` — from term/date info if available - `context.available_tech` — from detected resource types (LMS, video, discussions, etc.) diff --git a/skills/course-import/SKILL.md.tmpl b/skills/course-import/SKILL.md.tmpl index 182f600..b183bc9 100644 --- a/skills/course-import/SKILL.md.tmpl +++ b/skills/course-import/SKILL.md.tmpl @@ -710,10 +710,16 @@ to check the Bloom's levels and the 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`. +In this command, replace `` with the course title from the import. Then run the command. + ```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 "") +# Compute the course slug from this session's course title and prepare the export folder. +# Do not read project_name from the manifest here. Step 6 writes it, so on a first import +# the manifest has no name yet and the report lands in exports/untitled-course/. +IFS= read -r _PROJECT_NAME <<'IDSTACK_PROJECT_NAME' + +IDSTACK_PROJECT_NAME _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") _EXPORT_DIR=".idstack/exports/$_SLUG" _REPORT_PATH="$_EXPORT_DIR/course-import.html" @@ -768,7 +774,7 @@ Create or update the project manifest. **Fields populated by /idstack:course-import:** -- `project_name` — from course title +- `project_name` — from course title. Use the text that you put in place of `` in Step 5. - `context.modality` — inferred from course structure (async=online, sync sessions=hybrid) - `context.timeline` — from term/date info if available - `context.available_tech` — from detected resource types (LMS, video, discussions, etc.) diff --git a/skills/needs-analysis/SKILL.md b/skills/needs-analysis/SKILL.md index 6aeeca8..809f975 100644 --- a/skills/needs-analysis/SKILL.md +++ b/skills/needs-analysis/SKILL.md @@ -730,6 +730,8 @@ 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). +In this command, replace `` with the course title from Step 1. Then run the command. + ```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 @@ -751,8 +753,12 @@ fi for _p in "${CLAUDE_PLUGIN_ROOT}" "${IDSTACK_HOME:-}" "$_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 "") +# Compute the course slug from this session's course title and prepare the export folder. +# Do not read project_name from the manifest here. Step 7 writes it, so on a first run +# the manifest has no name yet and the report lands in exports/untitled-course/. +IFS= read -r _PROJECT_NAME <<'IDSTACK_PROJECT_NAME' + +IDSTACK_PROJECT_NAME _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") _EXPORT_DIR=".idstack/exports/$_SLUG" _REPORT_PATH="$_EXPORT_DIR/needs-analysis.html" @@ -822,6 +828,8 @@ Create or update the project manifest. Use the Write tool to write `.idstack/pro 6. If this is a new manifest, initialize ALL sections (including learning_objectives and quality_review) with empty/default values so downstream skills find the expected structure. +7. Set `project_name` to the text that you put in place of `` in + Step 6. The next skills use `project_name` to find the report folder. Write the manifest, then confirm to the user: diff --git a/skills/needs-analysis/SKILL.md.tmpl b/skills/needs-analysis/SKILL.md.tmpl index 23b200a..ea215f9 100644 --- a/skills/needs-analysis/SKILL.md.tmpl +++ b/skills/needs-analysis/SKILL.md.tmpl @@ -318,10 +318,16 @@ 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). +In this command, replace `` with the course title from Step 1. Then run the command. + ```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 "") +# Compute the course slug from this session's course title and prepare the export folder. +# Do not read project_name from the manifest here. Step 7 writes it, so on a first run +# the manifest has no name yet and the report lands in exports/untitled-course/. +IFS= read -r _PROJECT_NAME <<'IDSTACK_PROJECT_NAME' + +IDSTACK_PROJECT_NAME _SLUG=$("$_IDSTACK/bin/idstack-slugify" "$_PROJECT_NAME" 2>/dev/null || echo "untitled-course") _EXPORT_DIR=".idstack/exports/$_SLUG" _REPORT_PATH="$_EXPORT_DIR/needs-analysis.html" @@ -372,6 +378,8 @@ Create or update the project manifest. Use the Write tool to write `.idstack/pro 6. If this is a new manifest, initialize ALL sections (including learning_objectives and quality_review) with empty/default values so downstream skills find the expected structure. +7. Set `project_name` to the text that you put in place of `` in + Step 6. The next skills use `project_name` to find the report folder. Write the manifest, then confirm to the user: diff --git a/test/mutation-test.sh b/test/mutation-test.sh index da18b90..82a8ef9 100755 --- a/test/mutation-test.sh +++ b/test/mutation-test.sh @@ -1914,6 +1914,39 @@ PY regen expect_fail "a check step folds a missing checker into the python3 token" "$WORK/r/test/smoke-test.sh" "$WORK/r" +# 52a-52b. needs-analysis and course-import write project_name, and they write the +# report before the manifest. A slug that comes from the manifest is empty on a +# first run, so the report goes to exports/untitled-course/ and each later skill +# writes to exports//. Each skill has its own case, so a check that +# only reads the first skill cannot report GUARDED. +# 52a. needs-analysis reads the report slug from the manifest again -> smoke-test must fail. +fresh +python3 - "$WORK/r/skills/needs-analysis/SKILL.md.tmpl" <<'PY' +import sys +p = sys.argv[1]; s = open(p, encoding='utf-8').read() +old = "IFS= read -r _PROJECT_NAME <<'IDSTACK_PROJECT_NAME'\n\nIDSTACK_PROJECT_NAME\n" +new = '''_PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "")\n''' +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, new, 1) +open(p, 'w', encoding='utf-8').write(s) +PY +regen +expect_fail "needs-analysis reads the report slug from the manifest" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 52b. course-import reads the report slug from the manifest again -> smoke-test must fail. +fresh +python3 - "$WORK/r/skills/course-import/SKILL.md.tmpl" <<'PY' +import sys +p = sys.argv[1]; s = open(p, encoding='utf-8').read() +old = "IFS= read -r _PROJECT_NAME <<'IDSTACK_PROJECT_NAME'\n\nIDSTACK_PROJECT_NAME\n" +new = '''_PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.idstack/project.json')).get('project_name',''))" 2>/dev/null || echo "")\n''' +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, new, 1) +open(p, 'w', encoding='utf-8').write(s) +PY +regen +expect_fail "course-import reads the report slug from the manifest" "$WORK/r/test/smoke-test.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 f2893fd..4a57782 100755 --- a/test/smoke-test.sh +++ b/test/smoke-test.sh @@ -192,6 +192,20 @@ same = a and b and " ".join(a.group(1).split()) == " ".join(b.group(1).split()) sys.exit(0 if same else 1)' check "learning_preferences_note has the same words in manifest-schema.md and needs-analysis" "python3 -c '$NOTE_SYNC_PY' '$IDSTACK_DIR'" +# needs-analysis and course-import write project_name, and they write the +# report before the manifest. A slug read from the manifest at that point is +# empty on a first run: the report went to exports/untitled-course/ and every +# later skill wrote to exports//. The slug must come from the +# course title of the session, the same text the skill then writes as project_name. +SLUG_SOURCE_PY='import re, sys +s = open(sys.argv[1], encoding="utf-8").read() +blocks = [b for b in re.findall(r"```bash\n(.*?)```", s, re.S) if "idstack-slugify" in b] +ok = len(blocks) == 1 and ".idstack/project.json" not in blocks[0] and "" in blocks[0] +sys.exit(0 if ok else 1)' +for skill in needs-analysis course-import; do + check "$skill takes the report slug from the course title, not the manifest it writes later" "python3 -c '$SLUG_SOURCE_PY' '$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 From 603e9ccba7ab45f578197bc3f792cc4bf908fca7 Mon Sep 17 00:00:00 2001 From: Philippos Savvides Date: Wed, 7 Oct 2026 17:11:30 -0700 Subject: [PATCH 5/7] fix: cite the Sweller papers at their references.md tier Four skills cited [CogLoad-4] and [CogLoad-19] as T1, but references.md files both as T5, and course-builder gave two codes swapped tiers. The new test/check-citation-tiers.py reads each tier from references.md, and smoke-test runs it. Mutations 53a-53c guard the fix. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 1 + CONTRIBUTING.md | 1 + skills/course-builder/SKILL.md | 12 +-- skills/course-builder/SKILL.md.tmpl | 12 +-- skills/course-quality-review/SKILL.md | 2 +- skills/course-quality-review/SKILL.md.tmpl | 2 +- skills/learning-objectives/SKILL.md | 2 +- skills/learning-objectives/SKILL.md.tmpl | 2 +- skills/needs-analysis/SKILL.md | 10 +- skills/needs-analysis/SKILL.md.tmpl | 10 +- test/check-citation-tiers.py | 116 +++++++++++++++++++++ test/mutation-test.sh | 46 ++++++++ test/smoke-test.sh | 14 +++ 13 files changed, 204 insertions(+), 26 deletions(-) create mode 100755 test/check-citation-tiers.py diff --git a/CLAUDE.md b/CLAUDE.md index 16fa41b..ff4e0f1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,7 @@ Tests (run in CI on every push and PR — see `.github/workflows/test.yml`): # no sideways scroll, 44px touch targets, column counts (node + Chrome) python3 test/test-ste-check.py # bin/idstack-ste-check unit tests (smoke-test also runs them) python3 test/check-evidence-cards.py . # Verifies landing page evidence cards match evidence/references.md +python3 test/check-citation-tiers.py . # Verifies each [Code-N] [Tn] in skills, templates, README and landing page has its references.md tier python3 test/check-doc-accuracy.py . # Verifies documentation accuracy across version strings, binaries, flags, and links ./test/mutation-test.sh # Reintroduces each fixed defect and asserts its guarding test fails ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d4f9f1f..3b3e591 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -146,6 +146,7 @@ Every suite below runs in CI (`.github/workflows/test.yml`) on every push and pu | `test/test-rendered-landing.js` | Renders `docs/index.html` in headless Chrome across 11 widths and asserts the rendered outcome: no horizontal scroll, 44px touch targets, breakpoint column counts, sticky nav. Catches regressions the text suite cannot see (selector lists, `@container`, nested or print-only media queries, a `