@antoneeo/course-creator 0.1.0-beta.13 → 0.1.0-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.0-beta.15] - 2026-10-30
4
+
5
+ ### What's new
6
+
7
+ **An instructor-led course ships with everything its instructor needs.** A course taught by an instructor now comes with `INSTRUCTOR_GUIDE.md`. It tells the instructor what to prepare, the schedule, what not to show the learners, what to do when a live step goes wrong, and what to clean up afterwards. It also lists every material the delivery needs. The course check confirms that each listed material exists, so a missing file shows up before the session instead of during it.
8
+
9
+ **Feedback is attributed to the right version.** If a slide or unit names the course with a version different from the one the course declares (say "version 1.0" in a course at 1.2), the check names the section and the version it found. Feedback that quotes the version then reaches the version it is about.
10
+
11
+ **Demo projects stay out of the course's memory.** Materials can be listed anywhere in the project, so demo projects and the generated deck are listed where they are. If a listed demo project with its own repository sits inside `ai_docs/`, the check warns you: its documents would otherwise turn up in project searches as if they were the course's.
12
+
13
+ **From 0.1.0-beta.14, published without notes:**
14
+ - **The process checks can no longer be bypassed by accident.** Quoting a work level inside a sentence no longer counts as declaring it, and protected parts of the project stay protected when the agent changes folder.
15
+ - **You know what the project memory has not read yet.** The agent can tell how much of the project it has never examined.
16
+
17
+ #### What to do
18
+
19
+ **1. Update (everyone).** The changes arrive only with the update.
20
+ 1. Run `npm install -g @antoneeo/course-creator@latest`, then `course-creator-install-skill`.
21
+ 2. Close and reopen your agent sessions: the new version loads only at startup.
22
+
23
+ To check: `npm ls -g @antoneeo/course-creator` shows 0.1.0-beta.15.
24
+
25
+ **2. Write the instructor guide (instructor-led courses, and courses that already have one).**
26
+ - **This concerns you if** a course's content file (`SLIDE_CONTENT.md` or `COURSE_CONTENT.md`) declares `Delivery mode: instructor-led`: after the update, `check` reports `instructor-led course needs INSTRUCTOR_GUIDE.md before delivery`. It also concerns any course with a `Content contract` that already has an `INSTRUCTOR_GUIDE.md`, which is now checked. Courses that declare no `Content contract` are not affected.
27
+ - **Why:** the check refuses an instructor-led course without the guide, and refuses a guide written by hand in another shape, so the course fails `check` until the guide follows the template.
28
+ - **Steps.** The commands below use `<skill_dir>`, the folder where `course-creator-install-skill` placed the skill; you can also ask the agent to run them.
29
+ 1. Ask the agent: "write INSTRUCTOR_GUIDE.md for this course from the template in templates.md".
30
+ 2. Make sure `## Materials` lists every file and folder the delivery needs, with paths from the project root (for example `demo/TH-01/`). A folder path ends with `/`; a file path does not.
31
+ 3. If a listed demo project with its own `.git` or `ai_docs/` is inside `ai_docs/`, the check warns you. Move it outside, for example to `demo/` at the project root. Then update its path in `## Materials`, in any fallback cell and in any `ai_docs/...` path the guide cites. Finally run `python <skill_dir>/scripts/sdlc_check.py index --root .`, which regenerates the project memory without the demo files.
32
+ 4. If `check` reports `mentions course version X; Course version is Y`, find the line in the named section that names the course with version X, and correct it.
33
+ - **To check:** `python <skill_dir>/scripts/sdlc_check.py course <course-folder> --root .` reports `0 error(s)` for that course, and no warning about the guide. `<course-folder>` is the course's folder name under `ai_docs/solutions/courses/`.
34
+
35
+ **3. Protected folders in a repository with several `ai_docs/` (only if you use them; from 0.1.0-beta.14).**
36
+ - **This concerns you if** your agent settings contain `gate --hook --protected` and the repository has more than one `ai_docs/` folder, for example a package with its own.
37
+ - **Why:** protected paths are now read from the `ai_docs/` nearest to the edited file. A path written from the repository root, such as `packages/api/src/auth`, no longer matches the files inside `packages/api`, so the agent could change them without an analysis.
38
+ - **Steps:** rewrite those paths from the package: `packages/api/src/auth` becomes `src/auth`. Note that `src/auth` then protects the folder with that name in every project that has its own `ai_docs/`.
39
+ - **To check:** in a session with no open analysis, ask the agent for a small change to a protected file. It must be refused.
40
+
41
+ ### Added
42
+ - Instructor-led courses ship `INSTRUCTOR_GUIDE.md` (`templates.md`, F-086): what to prepare, the schedule, the reserved material (every solution unit by ID), what to do when a live step deviates and which fallback to use, what to clean up afterwards, and the list of every material the delivery needs with who may see it. `course_check` refuses a course whose content declares `Delivery mode: instructor-led` without the guide, and checks a guide wherever one exists in a course that declares a content contract: header equal to `COURSE_PLAN`, all six sections, every listed material present in the project, every cited `ai_docs/` path in the course folder or under a listed material, solution units reserved, deviation risks defined in `P-TM.md`, and scheduled modules present in `COURSE_PLAN` (`-` for a break or closing). A material may be anywhere in the project, so demo projects and the generated deck can be listed where they are. A listed folder inside `ai_docs/` that holds its own repository is warned, because project memory would index its documents as the course's. **An existing instructor-led course fails `check` until its guide is written to the template; a guide written by hand in another shape is refused too.** Courses without a declared content contract are unaffected.
43
+ - `course_check` refuses, in a course that declares a content contract (self-study or instructor-led), a line that names the course (the word course/corso, the folder slug or the full title) and mentions a dotted version other than the declared `Course version`, so feedback that quotes the version reaches the right one. A tool's version on a line that does not name the course, and a bare number such as `versione 2`, are not checked.
44
+
45
+ ### Changed
46
+ - The package's npm page (its README) gains a "What's new in <version>" section right after the introduction, written for the people who install the package and linking the full CHANGELOG. It appears from the first version that carries one; each version's notes are the `### What's new` at the top of its CHANGELOG section (F-070).
47
+
48
+ ## [0.1.0-beta.14] - 2026-10-27
49
+
50
+ ### Added
51
+ - Shared knowledge layer (F-084, kb CHANGELOG): `inventory <path>` and orient's upper bound of never-examined project files reach this lens; `validate`/`check` refuse a malformed `audit/INVENTORY_exclusions.md` row and the new `DECLARED`/`DIVERGES` claim rules; the docs site renders them. `sdlc_core.skip_dir` is extracted from `iter_files`, behaviour unchanged.
52
+ - `constraints --unit <id>` and `constraints --file <path>` (shared core): the constraints an ANALYSIS states in extractable form, each with its line, ending with the sections they could not extract.
53
+ - `benefit` and the docs site recognize a REVIEW_LOG row with tier `post-merge` (written by the code lens: the owner's report of a wrong behaviour in a merged change): `benefit` keeps it out of every review figure, the docs site shows it as "defect reported after merge". `review.md`: a reviewer derives callers and behaviours before comparing with the constraint view.
54
+ - `guides.md`: a code guide may carry a `## Preservation constraints` section copied from a closed unit; it survives regeneration. Nothing else changes in this lens: `validate` adds no note.
55
+
56
+ ### Fixed
57
+ - Shared claim checks (`check`): a `RULING` row on a note that names a foreign origin (`imported_from:`) is refused — relabelling an imported row is not ratifying it; the message for a row superseded by an `IMPORTED` one names the ratification act (F-083).
58
+ - Guide-verdict check (`verdict --hook`, shared core): a level quoted inside a sentence (`Level: L1` in prose) no longer counts as a declaration: only a line that opens with the level word does, the rule the constraint recall already uses (level 1 alone, `L1` or `E1`; levels 2 and 3 with `router` on the same line). The check now judges the project, and its docs-root name, that holds the edited file, not the one around the session's working directory; after a `cd` out of the project it let every edit through. A router verdict follows the same rule: only a line that opens with `router` or declares the level with it counts, never `router: no match` quoted in a sentence; `Level: Spike · router: …` and markdown openers (`##`, `1.`, `•`) count. A quote that itself opens a line (blockquote, code fence) still counts. The protected-path gate (`gate --hook`, `gate --file`) also judges the edited file's project, falls back to the session's project when no docs root sits above the file, and stays silent instead of exiting with an error when two docs roots sit side by side. Its `--protected` prefixes are relative to the nearest docs root above the edited file: with nested docs roots (a monorepo package with its own `ai_docs/`, or a docs folder inside a protected path) rewrite them relative to that root, or the path is no longer protected.
59
+
3
60
  ## [0.1.0-beta.13] - 2026-10-23
4
61
 
5
62
  ### Changed
package/README.md CHANGED
@@ -4,6 +4,47 @@
4
4
 
5
5
  The package is the fourth lens of the Agentic SDLC family. It uses the shared `ai_docs/` project memory and governance. Version 0.1.0-beta.4 supports Standalone mode. This is an experimental beta: the authoring method and artifact contracts may change; human learning efficacy has not been established. Its course artifacts remain readable without the skill. Each new course keeps a verbatim, append-only record of the requester's own words and decisions in `audit/requests/`, cited by the analysis and checked by its reviews.
6
6
 
7
+ <!-- whats-new:begin -->
8
+ <!-- generated by scripts/whats_new.js from distributions/course-creator/CHANGELOG.md: edit the CHANGELOG, then run node scripts/whats_new.js --write -->
9
+ ## What's new in 0.1.0-beta.15
10
+
11
+ **An instructor-led course ships with everything its instructor needs.** A course taught by an instructor now comes with `INSTRUCTOR_GUIDE.md`. It tells the instructor what to prepare, the schedule, what not to show the learners, what to do when a live step goes wrong, and what to clean up afterwards. It also lists every material the delivery needs. The course check confirms that each listed material exists, so a missing file shows up before the session instead of during it.
12
+
13
+ **Feedback is attributed to the right version.** If a slide or unit names the course with a version different from the one the course declares (say "version 1.0" in a course at 1.2), the check names the section and the version it found. Feedback that quotes the version then reaches the version it is about.
14
+
15
+ **Demo projects stay out of the course's memory.** Materials can be listed anywhere in the project, so demo projects and the generated deck are listed where they are. If a listed demo project with its own repository sits inside `ai_docs/`, the check warns you: its documents would otherwise turn up in project searches as if they were the course's.
16
+
17
+ **From 0.1.0-beta.14, published without notes:**
18
+ - **The process checks can no longer be bypassed by accident.** Quoting a work level inside a sentence no longer counts as declaring it, and protected parts of the project stay protected when the agent changes folder.
19
+ - **You know what the project memory has not read yet.** The agent can tell how much of the project it has never examined.
20
+
21
+ #### What to do
22
+
23
+ **1. Update (everyone).** The changes arrive only with the update.
24
+ 1. Run `npm install -g @antoneeo/course-creator@latest`, then `course-creator-install-skill`.
25
+ 2. Close and reopen your agent sessions: the new version loads only at startup.
26
+
27
+ To check: `npm ls -g @antoneeo/course-creator` shows 0.1.0-beta.15.
28
+
29
+ **2. Write the instructor guide (instructor-led courses, and courses that already have one).**
30
+ - **This concerns you if** a course's content file (`SLIDE_CONTENT.md` or `COURSE_CONTENT.md`) declares `Delivery mode: instructor-led`: after the update, `check` reports `instructor-led course needs INSTRUCTOR_GUIDE.md before delivery`. It also concerns any course with a `Content contract` that already has an `INSTRUCTOR_GUIDE.md`, which is now checked. Courses that declare no `Content contract` are not affected.
31
+ - **Why:** the check refuses an instructor-led course without the guide, and refuses a guide written by hand in another shape, so the course fails `check` until the guide follows the template.
32
+ - **Steps.** The commands below use `<skill_dir>`, the folder where `course-creator-install-skill` placed the skill; you can also ask the agent to run them.
33
+ 1. Ask the agent: "write INSTRUCTOR_GUIDE.md for this course from the template in templates.md".
34
+ 2. Make sure `## Materials` lists every file and folder the delivery needs, with paths from the project root (for example `demo/TH-01/`). A folder path ends with `/`; a file path does not.
35
+ 3. If a listed demo project with its own `.git` or `ai_docs/` is inside `ai_docs/`, the check warns you. Move it outside, for example to `demo/` at the project root. Then update its path in `## Materials`, in any fallback cell and in any `ai_docs/...` path the guide cites. Finally run `python <skill_dir>/scripts/sdlc_check.py index --root .`, which regenerates the project memory without the demo files.
36
+ 4. If `check` reports `mentions course version X; Course version is Y`, find the line in the named section that names the course with version X, and correct it.
37
+ - **To check:** `python <skill_dir>/scripts/sdlc_check.py course <course-folder> --root .` reports `0 error(s)` for that course, and no warning about the guide. `<course-folder>` is the course's folder name under `ai_docs/solutions/courses/`.
38
+
39
+ **3. Protected folders in a repository with several `ai_docs/` (only if you use them; from 0.1.0-beta.14).**
40
+ - **This concerns you if** your agent settings contain `gate --hook --protected` and the repository has more than one `ai_docs/` folder, for example a package with its own.
41
+ - **Why:** protected paths are now read from the `ai_docs/` nearest to the edited file. A path written from the repository root, such as `packages/api/src/auth`, no longer matches the files inside `packages/api`, so the agent could change them without an analysis.
42
+ - **Steps:** rewrite those paths from the package: `packages/api/src/auth` becomes `src/auth`. Note that `src/auth` then protects the folder with that name in every project that has its own `ai_docs/`.
43
+ - **To check:** in a session with no open analysis, ask the agent for a small change to a protected file. It must be refused.
44
+
45
+ Full history: [CHANGELOG](https://github.com/Antoneeo/agentic-sdlc-skill/blob/main/distributions/course-creator/CHANGELOG.md)
46
+ <!-- whats-new:end -->
47
+
7
48
  ## Install and initialize
8
49
 
9
50
  ```sh
@@ -23,5 +64,7 @@ Using the skill in your own project carries no obligation to publish your course
23
64
 
24
65
  New courses deliver a complete textual source for every slide (`SLIDE_CONTENT.md`), with exact learner copy, full explanations, examples, visual contents, transitions and answers. PPTX and other renderings are optional. Explicit non-slide requests retain a complete textual source for their units. Every course must show distinguishable value for its audience through an explanatory contribution and a transfer task; structure or simulated success does not establish human efficacy. See `skills/course-creator/slide_content.md`.
25
66
 
67
+ A course delivered by an instructor (`Delivery mode: instructor-led`) also ships `INSTRUCTOR_GUIDE.md`: what to prepare, the schedule, what to keep out of the learners' view, what to do when a live step fails, what to clean up afterwards, and the list of every material the delivery needs, wherever it is in the project, checked to exist. `course_check` refuses an instructor-led course without the guide, or with a guide that does not follow the template in `templates.md`. An existing instructor-led course therefore fails `check` until its guide is written to that template. In any course with a content contract it also refuses a line that names the course with a dotted version other than the declared `Course version`.
68
+
26
69
  Course design now uses outcome-first evidence, teaching/assessment alignment, visible worked reasoning, proportionate support, cognitive-load management and retrieval opportunities. `learning_design.md` records the pedagogical sources and the source-informed semantic review rubric; a locator table or structural PASS is not proof of learning.
27
70
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "course-creator",
3
- "version": "0.1.0-beta.13",
3
+ "version": "0.1.0-beta.15",
4
4
  "description": "Course creation for defined learners, with progressive explanations, sourced facts and diagnostic independent testing.",
5
5
  "author": "Antonio Pinto (https://github.com/Antoneeo)"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antoneeo/course-creator",
3
- "version": "0.1.0-beta.13",
3
+ "version": "0.1.0-beta.15",
4
4
  "description": "Design sourced, progressive courses for a defined learner with Agentic SDLC governance and diagnostic learner simulations.",
5
5
  "keywords": ["claude-code", "gemini-cli", "codex", "course-design", "skill", "ai-agent"],
6
6
  "author": "Antonio Pinto (https://github.com/Antoneeo)",
package/scripts/init.js CHANGED
@@ -1,62 +1,62 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { execFileSync } = require('child_process');
6
- const { SKILL_SOURCE, INSTALLED_SKILL_NAME, SELF_LENS, SIBLING_LENSES,
7
- CLIENTS, clientDetected, skillTarget, wireOrientHook, detectPython } = require('./lib');
8
-
9
- const root = process.cwd();
10
- const seed = new Map([
11
- ['ai_docs/README.md', `---\ndefault_domain: course\n---\n# Project knowledge and course work\n\nRead vision/project_vision.md, INDEX.md and reference/INDEX.md first.\n`],
12
- ['ai_docs/vision/project_vision.md', `---\ndescription: Project benefit and boundaries.\nstatus: DRAFT\n---\n# Project Vision\nStatus: DRAFT\n<!-- STUB: awaiting the owner interview (vision.md §The owner interview). Not a\n Vision: no artifact traces to it. -->\n`],
13
- ['ai_docs/vision/roadmap.md', `---\ndescription: Project learning and delivery roadmap.\nstatus: DRAFT\n---\n# Roadmap\nStatus: DRAFT\n`],
14
- ['ai_docs/vision/principles.md', `---\ndescription: Decisions that constrain project work.\nstatus: DRAFT\n---\n# Principles\nStatus: DRAFT\n`],
15
- ['ai_docs/strategic/architecture.md', `---\ndescription: Verified component inventory.\nstatus: CURRENT\n---\n# Architecture\n\n## Component Map\n| Component | Capability it owns | Contract | Where |\n|---|---|---|---|\n\n## Architectural Patterns\n<Add only verified patterns.>\n`],
16
- ['ai_docs/strategic/existing_features.md', `---\ndescription: Existing capabilities verified in this project.\nstatus: CURRENT\n---\n# Existing Features\n`],
17
- ['ai_docs/audit/audit_plan.md', `# Audit Plan\n\n| Area | State | Reference | Notes |\n|---|---|---|---|\n| . | PENDING | - | Initial analysis |\n`],
18
- ]);
19
- function writeIfAbsent(rel, content) {
20
- const dest = path.join(root, rel);
21
- if (fs.existsSync(dest)) return false;
22
- fs.mkdirSync(path.dirname(dest), { recursive: true });
23
- fs.writeFileSync(dest, content, 'utf8');
24
- console.log(`Created ${rel}`);
25
- return true;
26
- }
27
- for (const [rel, content] of seed) writeIfAbsent(rel, content);
28
- for (const dir of ['ai_docs/vision/features', 'ai_docs/reference', 'ai_docs/solutions/courses',
29
- 'ai_docs/audit/reviews']) fs.mkdirSync(path.join(root, dir), { recursive: true });
30
-
31
- const pointer = `# Course Creator project protocol\n\nUse the installed \`course-creator\` skill when the deliverable teaches a defined learner. It applies Agentic SDLC in Standalone mode: triage, project and course Vision, ANALYSIS, progressive explanations, sourced facts, independent diagnostic testing and a truthful efficacy label. Course files live in \`ai_docs/solutions/courses/<slug>/\`. Read \`ai_docs/README.md\` and the skill before working.\n\nInstall if missing: \`npm i -g @antoneeo/course-creator@beta && course-creator-install-skill\`.\n`;
32
- const pointers = { claude: 'CLAUDE.md', gemini: 'GEMINI.md', codex: 'AGENTS.md',
33
- antigravity: 'AGENTS.md' };
34
- const detected = CLIENTS.filter(clientDetected);
35
- for (const client of detected) writeIfAbsent(pointers[client.key], pointer);
36
- writeIfAbsent('.cursorrules', pointer);
37
- const siblings = new Map();
38
- for (const client of detected) {
39
- const parent = path.dirname(skillTarget(client));
40
- for (const [name, lens] of Object.entries(SIBLING_LENSES)) {
41
- if (fs.existsSync(path.join(parent, name))) siblings.set(name, lens);
42
- }
43
- }
44
- if (siblings.size) {
45
- const rows = [`- \`${INSTALLED_SKILL_NAME}\` — the **${SELF_LENS}** lens`,
46
- ...[...siblings].map(([name, lens]) => `- \`${name}\` — the **${lens}** lens`)];
47
- writeIfAbsent('AGENTIC_MULTI_LENS.md',
48
- `# Agentic SDLC family routing\n\nRead \`routing.md\` to choose the owner of each deliverable; \`memory.md\` is shared.\n\n${rows.join('\n')}\n`);
49
- }
50
- const python = detectPython();
51
- for (const client of detected.filter(c => c.key === 'claude')) {
52
- const result = wireOrientHook({ cwd: root, client, python, docsLabel: 'ai_docs' });
53
- console.log(`Claude SessionStart orientation: ${result.code}`);
54
- }
55
- if (python) {
56
- try {
57
- execFileSync(python, [path.join(SKILL_SOURCE, 'scripts/sdlc_check.py'),
58
- 'index', '--root', root], { stdio: 'ignore' });
59
- console.log('Generated ai_docs indexes.');
60
- } catch (e) { console.log('Run sdlc_check.py index when Python is available.'); }
61
- }
62
- console.log('Course Creator initialization complete.');
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execFileSync } = require('child_process');
6
+ const { SKILL_SOURCE, INSTALLED_SKILL_NAME, SELF_LENS, SIBLING_LENSES,
7
+ CLIENTS, clientDetected, skillTarget, wireOrientHook, detectPython } = require('./lib');
8
+
9
+ const root = process.cwd();
10
+ const seed = new Map([
11
+ ['ai_docs/README.md', `---\ndefault_domain: course\n---\n# Project knowledge and course work\n\nRead vision/project_vision.md, INDEX.md and reference/INDEX.md first.\n`],
12
+ ['ai_docs/vision/project_vision.md', `---\ndescription: Project benefit and boundaries.\nstatus: DRAFT\n---\n# Project Vision\nStatus: DRAFT\n<!-- STUB: awaiting the owner interview (vision.md §The owner interview). Not a\n Vision: no artifact traces to it. -->\n`],
13
+ ['ai_docs/vision/roadmap.md', `---\ndescription: Project learning and delivery roadmap.\nstatus: DRAFT\n---\n# Roadmap\nStatus: DRAFT\n`],
14
+ ['ai_docs/vision/principles.md', `---\ndescription: Decisions that constrain project work.\nstatus: DRAFT\n---\n# Principles\nStatus: DRAFT\n`],
15
+ ['ai_docs/strategic/architecture.md', `---\ndescription: Verified component inventory.\nstatus: CURRENT\n---\n# Architecture\n\n## Component Map\n| Component | Capability it owns | Contract | Where |\n|---|---|---|---|\n\n## Architectural Patterns\n<Add only verified patterns.>\n`],
16
+ ['ai_docs/strategic/existing_features.md', `---\ndescription: Existing capabilities verified in this project.\nstatus: CURRENT\n---\n# Existing Features\n`],
17
+ ['ai_docs/audit/audit_plan.md', `# Audit Plan\n\n| Area | State | Reference | Notes |\n|---|---|---|---|\n| . | PENDING | - | Initial analysis |\n`],
18
+ ]);
19
+ function writeIfAbsent(rel, content) {
20
+ const dest = path.join(root, rel);
21
+ if (fs.existsSync(dest)) return false;
22
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
23
+ fs.writeFileSync(dest, content, 'utf8');
24
+ console.log(`Created ${rel}`);
25
+ return true;
26
+ }
27
+ for (const [rel, content] of seed) writeIfAbsent(rel, content);
28
+ for (const dir of ['ai_docs/vision/features', 'ai_docs/reference', 'ai_docs/solutions/courses',
29
+ 'ai_docs/audit/reviews']) fs.mkdirSync(path.join(root, dir), { recursive: true });
30
+
31
+ const pointer = `# Course Creator project protocol\n\nUse the installed \`course-creator\` skill when the deliverable teaches a defined learner. It applies Agentic SDLC in Standalone mode: triage, project and course Vision, ANALYSIS, progressive explanations, sourced facts, independent diagnostic testing and a truthful efficacy label. Course files live in \`ai_docs/solutions/courses/<slug>/\`. Read \`ai_docs/README.md\` and the skill before working.\n\nInstall if missing: \`npm i -g @antoneeo/course-creator@beta && course-creator-install-skill\`.\n`;
32
+ const pointers = { claude: 'CLAUDE.md', gemini: 'GEMINI.md', codex: 'AGENTS.md',
33
+ antigravity: 'AGENTS.md' };
34
+ const detected = CLIENTS.filter(clientDetected);
35
+ for (const client of detected) writeIfAbsent(pointers[client.key], pointer);
36
+ writeIfAbsent('.cursorrules', pointer);
37
+ const siblings = new Map();
38
+ for (const client of detected) {
39
+ const parent = path.dirname(skillTarget(client));
40
+ for (const [name, lens] of Object.entries(SIBLING_LENSES)) {
41
+ if (fs.existsSync(path.join(parent, name))) siblings.set(name, lens);
42
+ }
43
+ }
44
+ if (siblings.size) {
45
+ const rows = [`- \`${INSTALLED_SKILL_NAME}\` — the **${SELF_LENS}** lens`,
46
+ ...[...siblings].map(([name, lens]) => `- \`${name}\` — the **${lens}** lens`)];
47
+ writeIfAbsent('AGENTIC_MULTI_LENS.md',
48
+ `# Agentic SDLC family routing\n\nRead \`routing.md\` to choose the owner of each deliverable; \`memory.md\` is shared.\n\n${rows.join('\n')}\n`);
49
+ }
50
+ const python = detectPython();
51
+ for (const client of detected.filter(c => c.key === 'claude')) {
52
+ const result = wireOrientHook({ cwd: root, client, python, docsLabel: 'ai_docs' });
53
+ console.log(`Claude SessionStart orientation: ${result.code}`);
54
+ }
55
+ if (python) {
56
+ try {
57
+ execFileSync(python, [path.join(SKILL_SOURCE, 'scripts/sdlc_check.py'),
58
+ 'index', '--root', root], { stdio: 'ignore' });
59
+ console.log('Generated ai_docs indexes.');
60
+ } catch (e) { console.log('Run sdlc_check.py index when Python is available.'); }
61
+ }
62
+ console.log('Course Creator initialization complete.');
package/scripts/lib.js CHANGED
@@ -3,8 +3,8 @@
3
3
 
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
- const os = require('os');
7
- const crypto = require('crypto');
6
+ const os = require('os');
7
+ const crypto = require('crypto');
8
8
  const { execSync } = require('child_process');
9
9
 
10
10
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
@@ -449,104 +449,104 @@ function pathsEqual(a, b) {
449
449
  return false;
450
450
  }
451
451
  }
452
- function pathInside(child, parent) {
452
+ function pathInside(child, parent) {
453
453
  try {
454
454
  const rel = path.relative(String(parent), String(child));
455
455
  return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
456
456
  } catch (e) {
457
457
  return false;
458
458
  }
459
- }
460
-
461
- // Ownership is per file. An existing directory without our marker belongs to its
462
- // owner, and a file edited after installation is retained on uninstall.
463
- const INSTALL_MARKER = '.course-creator-install.json';
464
- function hashFile(file) {
465
- return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
466
- }
467
- function sourceFiles(dir = SKILL_SOURCE, prefix = '') {
468
- const found = [];
469
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
470
- if (entry.isSymbolicLink()) continue;
471
- const rel = path.join(prefix, entry.name);
472
- if (entry.isDirectory()) found.push(...sourceFiles(path.join(dir, entry.name), rel));
473
- else if (entry.isFile()) found.push(rel);
474
- }
475
- return found;
476
- }
477
- function safeOwnedPath(target, rel) {
478
- if (path.isAbsolute(rel) || rel.split(/[\\/]/).includes('..')) return null;
479
- const full = path.resolve(target, rel);
480
- if (!pathInside(full, target)) return null;
481
- let cur = path.resolve(target);
482
- for (const part of path.relative(cur, full).split(path.sep)) {
483
- cur = path.join(cur, part);
484
- if (fs.existsSync(cur) && fs.lstatSync(cur).isSymbolicLink()) return null;
485
- }
486
- return full;
487
- }
488
- function ownedMarker(target) {
489
- const marker = path.join(target, INSTALL_MARKER);
490
- if (!fs.existsSync(marker)) return null;
491
- try {
492
- if (!fs.lstatSync(marker).isFile() || fs.lstatSync(marker).isSymbolicLink()) return null;
493
- const data = JSON.parse(fs.readFileSync(marker, 'utf8'));
494
- return data.skill === INSTALLED_SKILL_NAME && data.files &&
495
- typeof data.files === 'object' ? data : null;
496
- } catch (e) { return null; }
497
- }
498
- function installOwned(target) {
499
- if (!fs.existsSync(SKILL_SOURCE)) return { code: 'missing-source' };
500
- if (fs.existsSync(target) && (!fs.lstatSync(target).isDirectory() ||
501
- fs.lstatSync(target).isSymbolicLink())) return { code: 'unowned' };
502
- const existing = fs.existsSync(target) ? fs.readdirSync(target) : [];
503
- const prior = ownedMarker(target);
504
- if (existing.length && !prior) return { code: 'unowned' };
505
- fs.mkdirSync(target, { recursive: true });
506
- const owned = { ...(prior ? prior.files : {}) };
507
- let copied = 0, preserved = 0;
508
- for (const rel of sourceFiles()) {
509
- const dest = safeOwnedPath(target, rel);
510
- if (!dest) { preserved++; continue; }
511
- if (fs.existsSync(dest)) {
512
- if (!Object.hasOwn(owned, rel) || hashFile(dest) !== owned[rel]) {
513
- preserved++; continue;
514
- }
515
- }
516
- fs.mkdirSync(path.dirname(dest), { recursive: true });
517
- fs.copyFileSync(path.join(SKILL_SOURCE, rel), dest);
518
- owned[rel] = hashFile(dest);
519
- copied++;
520
- }
521
- fs.writeFileSync(path.join(target, INSTALL_MARKER),
522
- JSON.stringify({ skill: INSTALLED_SKILL_NAME, files: owned }, null, 2) + '\n');
523
- return { code: 'installed', copied, preserved };
524
- }
525
- function uninstallOwned(target) {
526
- if (!fs.existsSync(target) || fs.lstatSync(target).isSymbolicLink()) {
527
- return { code: 'unowned' };
528
- }
529
- const prior = ownedMarker(target);
530
- if (!prior) return { code: 'unowned' };
531
- let removed = 0, preserved = 0;
532
- for (const [rel, hash] of Object.entries(prior.files)) {
533
- const dest = safeOwnedPath(target, rel);
534
- if (!dest || !fs.existsSync(dest) || !fs.lstatSync(dest).isFile() ||
535
- hashFile(dest) !== hash) { preserved++; continue; }
536
- fs.unlinkSync(dest);
537
- removed++;
538
- }
539
- fs.unlinkSync(path.join(target, INSTALL_MARKER));
540
- // Remove only empty directories; a user-owned file keeps its parent.
541
- const dirs = new Set(Object.keys(prior.files).map(rel => path.dirname(rel)));
542
- for (const rel of [...dirs].sort((a, b) => b.length - a.length)) {
543
- const dir = path.resolve(target, rel);
544
- if (!pathInside(dir, target)) continue;
545
- try { fs.rmdirSync(dir); } catch (e) { /* non-empty or absent */ }
546
- }
547
- try { fs.rmdirSync(target); } catch (e) { /* user files remain */ }
548
- return { code: 'removed', removed, preserved };
549
- }
459
+ }
460
+
461
+ // Ownership is per file. An existing directory without our marker belongs to its
462
+ // owner, and a file edited after installation is retained on uninstall.
463
+ const INSTALL_MARKER = '.course-creator-install.json';
464
+ function hashFile(file) {
465
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
466
+ }
467
+ function sourceFiles(dir = SKILL_SOURCE, prefix = '') {
468
+ const found = [];
469
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
470
+ if (entry.isSymbolicLink()) continue;
471
+ const rel = path.join(prefix, entry.name);
472
+ if (entry.isDirectory()) found.push(...sourceFiles(path.join(dir, entry.name), rel));
473
+ else if (entry.isFile()) found.push(rel);
474
+ }
475
+ return found;
476
+ }
477
+ function safeOwnedPath(target, rel) {
478
+ if (path.isAbsolute(rel) || rel.split(/[\\/]/).includes('..')) return null;
479
+ const full = path.resolve(target, rel);
480
+ if (!pathInside(full, target)) return null;
481
+ let cur = path.resolve(target);
482
+ for (const part of path.relative(cur, full).split(path.sep)) {
483
+ cur = path.join(cur, part);
484
+ if (fs.existsSync(cur) && fs.lstatSync(cur).isSymbolicLink()) return null;
485
+ }
486
+ return full;
487
+ }
488
+ function ownedMarker(target) {
489
+ const marker = path.join(target, INSTALL_MARKER);
490
+ if (!fs.existsSync(marker)) return null;
491
+ try {
492
+ if (!fs.lstatSync(marker).isFile() || fs.lstatSync(marker).isSymbolicLink()) return null;
493
+ const data = JSON.parse(fs.readFileSync(marker, 'utf8'));
494
+ return data.skill === INSTALLED_SKILL_NAME && data.files &&
495
+ typeof data.files === 'object' ? data : null;
496
+ } catch (e) { return null; }
497
+ }
498
+ function installOwned(target) {
499
+ if (!fs.existsSync(SKILL_SOURCE)) return { code: 'missing-source' };
500
+ if (fs.existsSync(target) && (!fs.lstatSync(target).isDirectory() ||
501
+ fs.lstatSync(target).isSymbolicLink())) return { code: 'unowned' };
502
+ const existing = fs.existsSync(target) ? fs.readdirSync(target) : [];
503
+ const prior = ownedMarker(target);
504
+ if (existing.length && !prior) return { code: 'unowned' };
505
+ fs.mkdirSync(target, { recursive: true });
506
+ const owned = { ...(prior ? prior.files : {}) };
507
+ let copied = 0, preserved = 0;
508
+ for (const rel of sourceFiles()) {
509
+ const dest = safeOwnedPath(target, rel);
510
+ if (!dest) { preserved++; continue; }
511
+ if (fs.existsSync(dest)) {
512
+ if (!Object.hasOwn(owned, rel) || hashFile(dest) !== owned[rel]) {
513
+ preserved++; continue;
514
+ }
515
+ }
516
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
517
+ fs.copyFileSync(path.join(SKILL_SOURCE, rel), dest);
518
+ owned[rel] = hashFile(dest);
519
+ copied++;
520
+ }
521
+ fs.writeFileSync(path.join(target, INSTALL_MARKER),
522
+ JSON.stringify({ skill: INSTALLED_SKILL_NAME, files: owned }, null, 2) + '\n');
523
+ return { code: 'installed', copied, preserved };
524
+ }
525
+ function uninstallOwned(target) {
526
+ if (!fs.existsSync(target) || fs.lstatSync(target).isSymbolicLink()) {
527
+ return { code: 'unowned' };
528
+ }
529
+ const prior = ownedMarker(target);
530
+ if (!prior) return { code: 'unowned' };
531
+ let removed = 0, preserved = 0;
532
+ for (const [rel, hash] of Object.entries(prior.files)) {
533
+ const dest = safeOwnedPath(target, rel);
534
+ if (!dest || !fs.existsSync(dest) || !fs.lstatSync(dest).isFile() ||
535
+ hashFile(dest) !== hash) { preserved++; continue; }
536
+ fs.unlinkSync(dest);
537
+ removed++;
538
+ }
539
+ fs.unlinkSync(path.join(target, INSTALL_MARKER));
540
+ // Remove only empty directories; a user-owned file keeps its parent.
541
+ const dirs = new Set(Object.keys(prior.files).map(rel => path.dirname(rel)));
542
+ for (const rel of [...dirs].sort((a, b) => b.length - a.length)) {
543
+ const dir = path.resolve(target, rel);
544
+ if (!pathInside(dir, target)) continue;
545
+ try { fs.rmdirSync(dir); } catch (e) { /* non-empty or absent */ }
546
+ }
547
+ try { fs.rmdirSync(target); } catch (e) { /* user files remain */ }
548
+ return { code: 'removed', removed, preserved };
549
+ }
550
550
 
551
551
  // The marker lives under the family's agent-global root (the same root the
552
552
  // validator's AGENTIC_SDLC_KB_ROOT seam names), read at call time so the test
@@ -720,10 +720,10 @@ module.exports = {
720
720
  detectPython,
721
721
  pathsEqual,
722
722
  orientHookCommand,
723
- copyRecursive,
724
- installOwned,
725
- uninstallOwned,
726
- ownedMarker,
723
+ copyRecursive,
724
+ installOwned,
725
+ uninstallOwned,
726
+ ownedMarker,
727
727
  loadTemplates,
728
728
  templateFor,
729
729
  };
@@ -1,16 +1,16 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- const { CLIENTS, clientDetected, skillTarget, installOwned,
4
- wireGlobalOrientHook, detectPython } = require('./lib');
5
-
6
- for (const client of CLIENTS) {
7
- if (!clientDetected(client)) continue;
8
- const target = skillTarget(client);
9
- const result = installOwned(target);
10
- console.log(`${client.label}: ${result.code} (${target})`);
11
- if (client.key === 'claude' && result.code === 'installed') {
12
- try { wireGlobalOrientHook({ client, python: detectPython() }); }
13
- catch (e) { console.log('Session orientation can be wired from ENFORCEMENT.md.'); }
14
- }
15
- }
16
- console.log('Initialize a project with course-creator-init.');
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const { CLIENTS, clientDetected, skillTarget, installOwned,
4
+ wireGlobalOrientHook, detectPython } = require('./lib');
5
+
6
+ for (const client of CLIENTS) {
7
+ if (!clientDetected(client)) continue;
8
+ const target = skillTarget(client);
9
+ const result = installOwned(target);
10
+ console.log(`${client.label}: ${result.code} (${target})`);
11
+ if (client.key === 'claude' && result.code === 'installed') {
12
+ try { wireGlobalOrientHook({ client, python: detectPython() }); }
13
+ catch (e) { console.log('Session orientation can be wired from ENFORCEMENT.md.'); }
14
+ }
15
+ }
16
+ console.log('Initialize a project with course-creator-init.');
@@ -1,12 +1,12 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- const { CLIENTS, skillTarget, ownedMarker, uninstallOwned,
4
- removeGlobalOrientHooks } = require('./lib');
5
-
6
- const ownedTargets = CLIENTS.map(skillTarget).filter(ownedMarker);
7
- try { removeGlobalOrientHooks(ownedTargets); } catch (e) { /* convenience only */ }
8
- for (const target of ownedTargets) {
9
- const result = uninstallOwned(target);
10
- console.log(`${result.code}: ${target} (${result.removed} owned files removed; ` +
11
- `${result.preserved} retained)`);
12
- }
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const { CLIENTS, skillTarget, ownedMarker, uninstallOwned,
4
+ removeGlobalOrientHooks } = require('./lib');
5
+
6
+ const ownedTargets = CLIENTS.map(skillTarget).filter(ownedMarker);
7
+ try { removeGlobalOrientHooks(ownedTargets); } catch (e) { /* convenience only */ }
8
+ for (const target of ownedTargets) {
9
+ const result = uninstallOwned(target);
10
+ console.log(`${result.code}: ${target} (${result.removed} owned files removed; ` +
11
+ `${result.preserved} retained)`);
12
+ }
@@ -99,15 +99,15 @@ Semantics: exit code 2 + message on stderr ⇒ the write is blocked and the mess
99
99
 
100
100
  **Usage warnings:**
101
101
  - The gate is deliberately coarse: applied to all of `src/` it would also block the legitimate L1/L2 tasks foreseen by the Triage. Use it **only on security-critical directories** (`--protected "src/auth;src/crypto"`), where "never without analysis" is the desired policy.
102
- - The paths in `--protected` are prefixes relative to the project root, separated by `;`.
102
+ - The paths in `--protected` are prefixes relative to the root of the project the hook judges (below), separated by `;`. With nested docs roots (a monorepo whose package has its own), that is the nearest one above the edited file: write the prefixes relative to it.
103
103
  - `ai_docs/`, `tests/` and `test/` are always excluded from blocking.
104
- - The hook assumes the working directory is the project root (standard behavior of Claude Code hooks).
104
+ - The hook judges the project, and the docs root, holding the edited file: the session's working directory can move with a `cd`, so it is never the reference. With no docs root above the file, it is the session's own project, as before. `--root <path>` overrides it; `gate --file` resolves the same way, and two docs roots side by side leave the gate silent.
105
105
  - **Hybrid/devPNT projects**: add `--hybrid` to the gate command. Governed designs live in the devPNT DB, so the gate also unlocks when an approved E-TDD shadow (`ai_docs/solutions/SHADOW_*tdd*.md`, exported before implementation — in this package that clause is the whole statement of the duty; the sibling lenses keep it in their `hybrid.md`, which kb does not have) is present. Without the flag the gate would block legitimate governed work. The flag is deliberately explicit: never auto-detected.
106
106
 
107
107
  ### Verdict check: `verdict` (PreToolUse -- manual for this lens)
108
108
 
109
109
  `sdlc_check.py verdict --hook` refuses an Edit, Write, MultiEdit or NotebookEdit when the
110
- session transcript shows no guide-router verdict (`router: …`, or a level-1 declaration)
110
+ session transcript shows no guide-router verdict (a line that opens with `router: …` or with the level, `Level: L2 · router: …`, `Level: Spike · router: …`; or one that opens with a level-1 declaration)
111
111
  or a guide the verdict names was never read; it is silent in projects without guides and
112
112
  whenever it cannot tell. The code package (`agentic-sdlc`) wires it machine-wide on
113
113
  Claude Code; this package does not. To wire it by hand, add a `PreToolUse` entry with
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: course-creator
3
- version: 0.1.0-beta.13
3
+ version: 0.1.0-beta.15
4
4
  description: Design and improve courses that teach a defined learner through sourced, progressive explanations. Use when the deliverable is instruction or a learning path; route corpus intake to kb-agentic and persuasive campaigns to marketing-agentic.
5
5
  author: Antonio Pinto (https://github.com/Antoneeo)
6
6
  copyright: (c) 2026 Antonio Pinto
@@ -47,6 +47,7 @@ Before authoring a governed artifact, know which tier is authoring and follow `r
47
47
  | New production or substantial revision | Deliver the complete textual source in `slide_content.md`; declare its versioned content contract in COURSE_PLAN. |
48
48
  | A source or explanation changes | Update source locator, module and affected objective/check trace; keep a single authority for each fact. |
49
49
  | A course is tested | `SIMULATION_REPORT.md` names both independent attempts, control, material version and limitations; no simulated PASS changes efficacy status. |
50
+ | Instructor-led course, before delivery | `INSTRUCTOR_GUIDE.md` from `templates.md`: preparation, schedule, reserved material, deviations with their fallback, after the course, and the verified list of the delivered materials. `course_check` refuses an instructor-led course without it. |
50
51
  | Real feedback arrives | `FEEDBACK_REPORT.md` identifies course/version/module and evidence; propose a course correction or a method improvement. |
51
52
  | L3 design or closure review | Append a `REVIEW_LOG.md` row with findings, tier, reviewer and verdict before claiming the gate passed. |
52
53
  | Concurrent work | Write `audit/HANDOFF_[course].md` with resume logistics; `audit/handoff.md` is the generated workstream registry rebuilt by `index`. Its `next:` is the next concrete step in one sentence; what happened, and the state of the work, go to the Diary as a dated entry. |