@antoneeo/course-creator 0.1.0-beta.3 → 0.1.0-beta.5

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.0-beta.5] - 2026-10-17
4
+
5
+ ### Changed
6
+ - F-073: one written form for a dated entry, `**YYYY-MM-DD — label.**`, is taught for Diary entries, spike outcomes and project notes that record an event, and shared readers (`entry_openers`, `request_entries`, `adr_date`, `frontmatter_date`) let a chronology list those events the same way the validator reads them. Three new advisories, which never fail a build, not even `--strict`: undated text in the Diary of an L3 analysis started on or after 2026-10-18; a HANDOFF `next:` longer than 240 characters on an L3 file updated on or after that date (`next:` now holds only the next step, and the state of the work goes to the Diary); and, in `check` only, an open L3 unit whose id another branch or tag uses for a different analysis (one `git grep` over local refs, silent without git). The doctrine now says to choose a new unit's id after fetching, and that a session sharing a checkout with another session works in its own git worktree.
7
+ - The package publishes on npm `latest`, like the other three: `npm i @antoneeo/course-creator` installs the newest version with no separate promotion. The version keeps its `-beta.N` suffix and the README its experimental statement; the `beta` dist-tag no longer advances.
8
+
9
+ ## [0.1.0-beta.4] - 2026-10-17
10
+
11
+ ### Changed
12
+ - F-068 (shared doctrine with kb-agentic): the supersession cascade runs at the unit's closure, once for every supersession since the last one, instead of at each supersession.
13
+ - F1: closing the last open workstream now empties the generated registry `audit/handoff.md` instead of leaving its stale row behind: with zero `HANDOFF_*.md` sources `index` rewrites it empty (keeping `project_notes.md`), and `validate` reports a generated registry that still has rows but no sources as an error.
14
+ - F-069: `graph`, `check`, `stale` and the session-start count are faster on a mature knowledge base, with byte-identical output. Citations of superseded claim ids are found with one pass per living document instead of one scan per (document, id) pair, and one check run reads and resolves each cited extraction once instead of once per claim row. On a synthetic ledger of 2183 rows and 1000 living documents, `graph` drops from 14 s to 4 s. No new flag.
15
+
3
16
  ## [0.1.0-beta.3] - 2026-10-13
4
17
 
5
18
  ### Changed
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `course-creator` helps an AI agent create a course for a defined learner. It maps what the learner already knows, orders concepts by prerequisite, writes explanations in progressive modules, and links factual statements to reopenable sources. Independent fresh learner agents can expose confusing steps; their results are diagnostic. The course is labeled **efficacy not verified** until enough evidence from real learners supports a narrower claim.
4
4
 
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.3 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.
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
7
  ## Install and initialize
8
8
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "course-creator",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.5",
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,11 +1,11 @@
1
1
  {
2
2
  "name": "@antoneeo/course-creator",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.5",
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)",
7
7
  "license": "Apache-2.0",
8
- "publishConfig": { "access": "public", "tag": "beta" },
8
+ "publishConfig": { "access": "public" },
9
9
  "bin": {
10
10
  "course-creator-init": "scripts/init.js",
11
11
  "course-creator-install-skill": "scripts/postinstall.js"
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\n## Expected Benefit\n<Discuss with the owner.>\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\n## Expected Benefit\n<Discuss with the owner.>\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
+ }
@@ -47,8 +47,8 @@ The consolidated view is `sdlc_check.py stale` (the overlay intercepts
47
47
  the spine command, preserves its output, flags and exit code --
48
48
  `--hybrid` included -- and appends `## claims` only when nonempty). The
49
49
  orient append prints one count line at session start, only when dirty
50
- and always fail-open. Event-driven by design: reconciliation §2 runs
51
- `stale` after recording a supersession; nothing polls per turn.
50
+ and always fail-open. Event-driven by design: reconciliation §2 runs `stale` at the unit's
51
+ closure, once for every supersession since the last one; nothing polls per turn.
52
52
 
53
53
  ## 2. Check in CI (recommended for teams)
54
54
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: course-creator
3
- version: 0.1.0-beta.3
3
+ version: 0.1.0-beta.5
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
@@ -46,7 +46,7 @@ Before authoring a governed artifact, know which tier is authoring and follow `r
46
46
  | A course is tested | `SIMULATION_REPORT.md` names both independent attempts, control, material version and limitations; no simulated PASS changes efficacy status. |
47
47
  | Real feedback arrives | `FEEDBACK_REPORT.md` identifies course/version/module and evidence; propose a course correction or a method improvement. |
48
48
  | L3 design or closure review | Append a `REVIEW_LOG.md` row with findings, tier, reviewer and verdict before claiming the gate passed. |
49
- | Concurrent work | Write `audit/HANDOFF_[course].md` with resume logistics; `audit/handoff.md` is the generated workstream registry rebuilt by `index`. |
49
+ | 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. |
50
50
  | A repeatable lesson from source or work | PROPOSE distilling a guide with provenance under `reference/`; Propose proactively when the knowledge would otherwise be lost. |
51
51
 
52
52
  An operative guide is not a dumping ground. Follow `guides.md`: source provenance and section-level fidelity make a guide useful in a later session.
@@ -89,7 +89,7 @@ Run `python <skill_dir>/scripts/sdlc_check.py course <slug> --root <project_root
89
89
 
90
90
  Read `feedback.md` when real learner feedback or observed use arrives. A single reproducible defect may justify correcting the course or proposing a skill guide or change. It does not prove effectiveness. Human effectiveness needs comparable learners, observable criteria, results including contrary cases and stated limits; a formal pilot is optional. Keep **efficacy not verified** until that evidence supports a different claim.
91
91
 
92
- Run `sdlc_check.py index` then `sdlc_check.py check`; include course material, Vision, ANALYSIS and documentation in the same change. Close with an independent diff review against the approved design, tests, and a truthful efficacy label. Isolate the work on its own branch/worktree for L3. Branch/worktree hygiene follows the project owner’s integration choice; do not discard unmerged work without authorization.
92
+ Run `sdlc_check.py index` then `sdlc_check.py check`; include course material, Vision, ANALYSIS and documentation in the same change. Close with an independent diff review against the approved design, tests, and a truthful efficacy label. Isolate the work on its own branch/worktree for L3. A session that may run while another session uses the same checkout works in its **own git worktree**, and never switches branch, stashes, resets or runs `index` in a checkout another session may be using. Parallel work also shares ids: choose a new unit's id **after fetching**, as the first id free on every branch and tag the checkout can see (`check` names a collision while the unit is open). Branch/worktree hygiene follows the project owner’s integration choice; do not discard unmerged work without authorization.
93
93
 
94
94
  ## Operative Guides and memory
95
95