Skip to content

fix: audit remediation + v3.3.0.0 — course memory, pipeline orchestration, re-run detection - #57

Merged
savvides merged 11 commits into
mainfrom
fix/audit-v3.3.0.0
Aug 5, 2026
Merged

fix: audit remediation + v3.3.0.0 — course memory, pipeline orchestration, re-run detection#57
savvides merged 11 commits into
mainfrom
fix/audit-v3.3.0.0

Conversation

@savvides

@savvides savvides commented Aug 5, 2026

Copy link
Copy Markdown
Owner

A full audit of the toolchain (generation pipeline, install/test infrastructure, skill content contracts) found the architecture sound but surfaced a set of bugs that broke user-visible behavior — several since the features shipped — plus the test-infrastructure gaps that let them ship.

The bugs

What was broken Effect
templates/preamble.md f-string with nested same-type quotes SyntaxError on every Python < 3.12 (macOS ships 3.9). The whole context-recovery block died behind 2>/dev/null, so welcome-back messages, quality trends, and next-skill suggestions never worked for most users.
pipeline invoked children as skill: "needs-analysis" Plugin skills need idstack:needs-analysis. The orchestrator's execution loop could not resolve a single child skill in Claude Code.
Five skills checked non-canonical manifest section names "Update the results or start fresh?" never fired for assessment-design, course-builder, course-export, course-import, course-quality-review.
learn + course-export $_IDSTACK path list omitted the marketplace cache On the way most users install, those skills' bin/ calls pointed at a nonexistent directory.
bin/idstack-status interpolated $PROJECT_NAME into Python source A course named "Bob's Course" blanked the entire dashboard.
bin/idstack-doctor used a fixed grep -A4 window With another plugin listed right after idstack, the neighbour's enabled was read as idstack's — a disabled install diagnosed as healthy.
setup --keep-legacy honored in 1 of 3 deletion paths; --local deleted a symlink under $HOME; a nonzero claude exit aborted setup silently before the Codex step.

Why they shipped, and what changed

The suite had never run automatically — no CI at all — and parts of it were not testing what they appeared to test. test-version-classifier.sh tested a local copy of the classifier, so it stayed green while the two real call sites could drift. gen-skills counted a placeholder-less template as neither generated nor failed, so --dry-run passed over absent output.

  • CI added (test.yml): all seven suites on push and PR, ubuntu + macos, Python 3.9 and 3.12. The 3.9 leg is what catches the context-recovery class of bug. release.yml now refuses to publish unless tag == VERSION == plugin.json == CHANGELOG and the smoke test passes.
  • ./setup is tested — 16 behavioral tests against a repo copy with a fake $HOME and a stub claude. It had zero coverage while the smoke test spent 14 assertions on landing-page CSS.
  • A mutation suite (test/mutation-test.sh) reintroduces each of the 13 defects into a throwaway copy and asserts the guarding test fails. All 13 are caught. A guard nobody has seen fail is not a guard.
  • Untestable logic extracted to bin/lib/version-classify.sh and plugin-status.sh — sourced by their callers, so the unit tests exercise shipped code.
  • Drift made structurally impossible: a new {{IDSTACK_RESOLVE}} generator placeholder splices one canonical $_IDSTACK resolution into every bash block that needs it (blocks run in separate shells, so each must be self-contained), and smoke-test bans hand-rolled derivations.

smoke-test: 272 → 371 assertions, and it now prints diagnostics instead of a bare FAIL.

Verification

  • All seven suites pass locally; bash -n clean on every script; gen-skills --dry-run reports 0 stale across all 22 generated files.
  • Merge-tool exit codes cited in skill text (4 = manifest not found, 1 = malformed payload) verified by execution, as were both --payload forms.
  • Codex flavor verified to differ from the Claude flavor only by the stripped allowed-tools block, per skill.
  • The preamble fix proven on the exact interpreter that was failing (Python 3.9): the old block produced empty output and exit 0; the new one prints SKILLS_COMPLETED and SUGGESTED_NEXT.

Note on the release

plugin.json is bumped to 3.3.0.0 — marketplace installs cache a versioned snapshot, so without that bump none of these fixes reach users. Existing users update with git pull && ./setup.

🤖 Generated with Claude Code

savvides and others added 11 commits August 4, 2026 18:32
- smoke/integration check() now captures and prints failure diagnostics
  instead of discarding stderr
- migration block runs under a trap with per-check guards so a migrate
  failure records a FAIL instead of killing the suite and leaking a tempdir
- version assertions read VERSION instead of hardcoding v3.2.0.0; new
  VERSION/plugin.json/CHANGELOG agreement checks
- new v1.1 migration test exercising the failed_items int->list conversion
  (fixture existed but was never loaded)
- integration-test: stale-file test runs in a sandbox copy, never mutating
  the tracked tree; trap quoting fixed for paths with spaces
- version classifier extracted to bin/lib/version-classify.sh, sourced by
  setup, idstack-doctor, and the unit test - the test now exercises the
  shipped code instead of a local mirror
- new .github/workflows/test.yml (ubuntu/macos, python 3.9/3.12) running
  all four suites on push and PR
- release.yml now verifies tag == VERSION == plugin.json == CHANGELOG and
  runs the smoke test before publishing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…or, setup

- idstack-status: PROJECT_NAME now travels via the environment instead of
  being interpolated into Python source; an apostrophe in a course name no
  longer blanks the dashboard with a SyntaxError. Readiness block failure
  prints a message instead of vanishing behind '|| true'. course-import
  added to the pipeline progression (it joins before learning-objectives).
- idstack-gen-skills: a template missing {{PREAMBLE}} is now an ERROR
  counted by --dry-run and generation (was a silent SKIP that let the
  drift gate pass green over an absent output); templates/report-format.md
  added to the fail-loud preflight; stale marketplace.json comment removed.
- idstack-doctor: plugin version parsed with python3 json (grep fallback
  without python3); enabled-detection scoped to idstack's own list entry
  instead of a fixed -A4 window; vestigial-symlink scan is scope-aware.
- setup: --keep-legacy honored in all three legacy-removal paths (was 1 of
  3); vestigial-symlink cleanup is scope-aware so --local never touches
  $HOME; 'claude plugin marketplace add'/'plugin install' failures now
  error loudly with a manual-recovery hint instead of aborting silently
  under set -e; dead duplicate rm branch collapsed; Codex link count
  reported from actual links created.
- integration-test: new regression tests for the apostrophe project name,
  the course-import suggestion, and the missing-{{PREAMBLE}} error path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Preamble (affects every generated skill):
- context-recovery f-string with nested same-type quotes replaced with
  concatenation; it was a SyntaxError on every Python < 3.12 (macOS system
  python3 is 3.9) that died silently behind 2>/dev/null, killing welcome-back
  messages, quality trends, and next-skill suggestions since it shipped
- SUGGESTED_NEXT now handles import-first projects (course-import joins the
  chain before learning-objectives)
- every preamble bash block re-derives $_IDSTACK — blocks run in separate
  shells, so a value derived in an earlier block was never actually available

Canonical $_IDSTACK resolution:
- new {{IDSTACK_RESOLVE}} generator placeholder spliced from
  templates/snippets/idstack-resolve.sh into all 47 bash blocks that call
  $_IDSTACK/bin tools; learn's and course-export's hand-rolled path lists
  (which omitted the marketplace cache — the way most users install — and
  included a banned legacy path) are replaced by it. Drift is now
  structurally impossible and smoke-test bans raw _IDSTACK= in templates.

Pipeline orchestrator:
- child skills invoked as skill: "idstack:<name>" — the bare name never
  resolved in Claude Code, breaking the orchestrator's execution loop
- status table, announcements, and resume hint use /idstack: forms
  (including the '/idstack pipeline' space typo); pipeline now logs its own
  completion to the timeline

Contract fixes across skills:
- five skills checked non-canonical manifest section names in their re-run
  guard (assessment_design, course_builder, course_export, course_import,
  course_quality_review) — re-run detection never fired for them; corrected
  to the canonical names, plus prose sites and red-team's nonexistent
  course_builder.output_path (now course_content.content_dir)
- 8 body-duplicated 'Preamble: Project Manifest' sections slimmed to their
  skill-specific deltas
- course-quality-review and course-export (single-section owners) now write
  through bin/idstack-manifest-merge; needs-analysis and learning-objectives
  document their Write-tool fallback the way course-import models it
- user-facing /skill refs namespaced to /idstack:<skill> (~45 sites) so the
  Codex translation rule has a prefix to strip
- learning-objectives: missing 'Top recommendations' report section added;
  [Alignment-1] tier corrected from T2 to T5 (Biggs 1996) at both cite sites
- manifest-schema.md report_path claim corrected (idstack-status globs the
  export folder); index.html.tmpl carries a concrete course-import row shape

Tests:
- new test/test-preamble-python.sh runs the context-recovery block on the
  host interpreter with positive output assertions (the old block produced
  empty output on 3.9 and exited 0 — verified); wired into smoke + CI
- smoke-test: 70 new assertions pinning canonical section names, /idstack:
  namespacing, the resolve-snippet lockstep, merge-tool usage, and Top
  recommendations; integration-test tree check compares before/after state

All 22 generated files regenerated in this commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- README: drop the hardcoded 'v2.5.0' status/new-in block (two releases
  stale); point at CHANGELOG.md so the README can't drift again
- TODOS: mark the v2.4 .idstack/reports/ description as historical — the
  current contract is HTML reports under .idstack/exports/<course-slug>/
- report-format.md: idstack-status discovers reports by globbing the export
  folder, not by reading report_path — say so

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
VERSION, plugin.json (the line marketplace installs actually serve),
CHANGELOG entry, and landing page (hero badge, what's-new card, JSON-LD
softwareVersion/dateModified). smoke-test needed no edit — its version
assertions read VERSION now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The enabled-detection logic replaced in the previous commit was correct but
untestable while inlined in idstack-doctor. Extracted behind plugin_is_enabled
following the version-classify.sh precedent, with 9 pinned cases.

Demonstrated the bug the scoping fixes: given

    idstack@idstack
      Status: disabled
    superpowers@marketplace
      Status: enabled

the old 'grep -A4' window reads the neighbour's status line and reports a
disabled idstack as 'installed and enabled' — the doctor gives a clean bill
of health for an install that cannot work. The scoped window ends the entry
at a blank line or the next name@marketplace id and returns disabled.

Also pins: doctor sources the shared parser, and no 'grep -A4' remains.
smoke-test 365 -> 370 assertions; new suite wired into CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…3.0.0

The doctor's fixed-window enabled-check was a user-facing bug (a broken
install diagnosed as healthy) and was missing from the entry. Also corrects
the suite count (six) and assertion count (370) after test-plugin-status.sh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reintroduces all 10 defects fixed in this branch into a throwaway copy and
asserts the guarding test fails: preamble f-string, course-import suggestion,
non-canonical section names, bare /skill refs, pipeline namespace, hand-rolled
_IDSTACK, plugin-status fixed window, version disagreement, stale generated
file, missing Top recommendations. All 10 are caught.

This is the check that was missing before: the version-classifier suite
passed green while testing a copy of the classifier rather than the shipped
code, and gen-skills counted a placeholder-less template as neither generated
nor failed. A guard nobody has ever seen fail is not a guard.

Kept out of smoke-test (copies the repo per mutation); runs as its own CI job.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
16 behavioral tests against a repo copy with a fake $HOME and a stub
`claude` on PATH, so neither the real install nor the working tree is
touched: flag parsing (unknown flag exits 2, --help documents --keep-legacy),
scope selection (user vs project), all three legacy-cleanup shapes with and
without --keep-legacy, the unrecognized-directory guard, --local not touching
$HOME, loud failure when `claude` exits nonzero, and the post-setup
regeneration invariant.

This was the audit's coverage inversion: 14 smoke assertions on landing-page
CSS, zero on the script every new user runs first — which is how four defects
lived in it.

Three matching mutations added (per-skill loop ignoring --keep-legacy, --local
leaking into $HOME, silent `claude` failure); all 13 mutations now caught.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seven suites in CI (setup + plugin-status added since the entry was written),
371 smoke assertions, and the mutation suite documented as the check that
proves the guards actually guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eight confirmed findings from the review pass, plus the CI failure the new
mutation job caught.

Correctness:
- bin/idstack-migrate --init creates a canonical manifest (every section at
  its default) when none exists. Three skills instructed the user to run
  idstack-migrate to create one in standalone mode; it was a no-op on a
  missing file, so the merge that followed exited 4 and standalone results
  were silently never persisted. The skeleton is produced by running the
  existing migration chain over a minimal seed, so 'canonical' has one
  definition. accessibility-review, assessment-design, and red-team now call
  it; accessibility-review and assessment-design also regained the standalone
  persistence note that the section-slimming dropped.
- $_IDSTACK marketplace-cache resolution sorted lexically while its comment
  claimed 'highest version' — it ranks 3.9.0.0 above 3.10.0.0 and would pick
  a stale install once the minor hits double digits. Now sorts basenames by
  numeric version field. Preamble's three copies re-synced.
- templates/manifest-schema.md is spliced verbatim into 20 skills (getline
  bypasses placeholder expansion), so its merge example called $_IDSTACK with
  nothing resolving it. The resolution is now written out inline, with a note
  explaining why the placeholder is not used there.
- bin/idstack-status printed bare /skill commands (checkboxes, suggested next,
  readiness verdict) — the exact form this branch bans in skill text because
  it is not a valid command. All namespaced; integration-test asserts no bare
  form can return.

Test integrity — three ways the suite was weaker than it looked:
- The mutation job failed in CI and was right to: pinned to Python 3.12,
  where PEP 701 makes the nested-quote f-string legal, so reintroducing that
  bug is a no-op and reported NOT-GUARDED. Job moved to 3.9 (where the bug
  exists) and the case now skips explicitly with a reason on 3.12+.
- Five mutations edited a .tmpl without regenerating, so they were caught by
  the staleness gate rather than the assertion under test — GUARDED proved
  nothing about that assertion. All template mutations now regenerate first;
  still 13/13 caught, now for the right reason.
- test-setup's 'leaves generated files fresh' check asserted the fixture was
  fresh, not that setup regenerated: it passed with setup's regeneration call
  deleted. It now dirties a generated file first and asserts staleness before
  the run.

Also: manifest-merge suite covers --init (creation, schema version, every
mergeable section present, merge-after-init, non-clobbering, and that plain
migrate still no-ops); CHANGELOG corrected — learning-objectives moved to the
merge path and course-import, not learning-objectives, is the second
Write-tool-fallback skill.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@savvides
savvides merged commit 5071975 into main Aug 5, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant