@antoneeo/course-creator 0.1.0-beta.2 → 0.1.0-beta.4
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 +12 -0
- package/README.md +1 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/scripts/init.js +62 -62
- package/scripts/lib.js +98 -98
- package/scripts/postinstall.js +16 -16
- package/scripts/preuninstall.js +12 -12
- package/skills/course-creator/ENFORCEMENT.md +2 -2
- package/skills/course-creator/SKILL.md +3 -2
- package/skills/course-creator/elicitation.md +7 -3
- package/skills/course-creator/review.md +9 -1
- package/skills/course-creator/scripts/course_check.py +132 -132
- package/skills/course-creator/scripts/knowledge.py +68 -28
- package/skills/course-creator/scripts/sdlc_core.py +53 -6
- package/skills/course-creator/scripts/shared_manifest.json +5 -5
- package/skills/course-creator/templates.md +39 -0
- package/skills/course-creator/vision.md +245 -245
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.0-beta.4] - 2026-10-17
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- 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.
|
|
7
|
+
- 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.
|
|
8
|
+
- 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.
|
|
9
|
+
|
|
10
|
+
## [0.1.0-beta.3] - 2026-10-13
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- F-067: every L3 unit now keeps a verbatim, append-only request record `audit/requests/REQUEST_course_<slug>.md`, started before the Vision Gate: the owner's opening words and every later statement that settles something (answers, approvals, costs, rulings). Artifacts cite its entries instead of paraphrasing; the shared `review.md` asks for the record and makes an owner statement with no resolving entry a finding. Secrets, personal data and confidential third-party or employer information are redacted.
|
|
14
|
+
|
|
3
15
|
## [0.1.0-beta.2] - 2026-10-08
|
|
4
16
|
|
|
5
17
|
### 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.
|
|
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
|
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "course-creator",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.4",
|
|
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.
|
|
3
|
+
"version": "0.1.0-beta.4",
|
|
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\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
|
};
|
package/scripts/postinstall.js
CHANGED
|
@@ -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.');
|
package/scripts/preuninstall.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
version: 0.1.0-beta.4
|
|
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
|
|
@@ -38,6 +38,7 @@ Before authoring a governed artifact, know which tier is authoring and follow `r
|
|
|
38
38
|
|
|
39
39
|
| When | Write or update |
|
|
40
40
|
|---|---|
|
|
41
|
+
| New L3 unit, the moment the level is declared | `audit/requests/REQUEST_course_<slug>.md`: R1 quotes the requester's opening words verbatim, before the Vision Gate and the first question; every later statement of theirs that settles something (answer, approval, cost, ruling) is appended before acting on it. Append-only, never deleted; artifacts cite its entries. |
|
|
41
42
|
| New L3 course | `vision/features/VISION_course_<slug>.md` with explicit `domain: course`; approve it before design. |
|
|
42
43
|
| Approved Vision and a design need | `solutions/ANALYSIS_course_<slug>.md` with course risks; `D-UC.md`, `D-IC.md`, `P-TM.md`, graph and plan detail its IDs. |
|
|
43
44
|
| New production or substantial revision | Deliver the complete textual source in `slide_content.md`; declare its versioned content contract in COURSE_PLAN. |
|
|
@@ -58,7 +59,7 @@ Read `ai_docs/README.md`, `ai_docs/INDEX.md`, and `ai_docs/reference/INDEX.md` (
|
|
|
58
59
|
|
|
59
60
|
### 2. Vision Gate
|
|
60
61
|
|
|
61
|
-
Draft the course Vision with benefit, named learner groups, what they should be able to understand and do, and signals that would count as useful learning. Read `vision.md`, run its blind check, and obtain the person's approval before writing the course design. Project Vision remains above the course Vision. A course may be delivered before human evaluation, visibly labeled **efficacy not verified**.
|
|
62
|
+
Draft from the request as recorded in `audit/requests/REQUEST_course_<slug>.md`, not from a paraphrase of it. Draft the course Vision with benefit, named learner groups, what they should be able to understand and do, and signals that would count as useful learning. Read `vision.md`, run its blind check, and obtain the person's approval before writing the course design. Project Vision remains above the course Vision. A course may be delivered before human evaluation, visibly labeled **efficacy not verified**.
|
|
62
63
|
|
|
63
64
|
### 3. Course Design
|
|
64
65
|
|
|
@@ -67,14 +67,17 @@ Ask it before the first write that would embed its answer, grouped with every
|
|
|
67
67
|
doubt open at that point, in one numbered set, with the client's
|
|
68
68
|
structured-question facility where it has one. Never defer a doubt to the
|
|
69
69
|
deliverable, where the practitioner must find it inside the document; never drip
|
|
70
|
-
one question per turn.
|
|
70
|
+
one question per turn. At L3 record every answer verbatim in the request record
|
|
71
|
+
(`audit/requests/REQUEST_course_<slug>.md`) and fold it into the artifact citing that
|
|
72
|
+
entry; an
|
|
71
73
|
intent choice with neither a source nor a weighing is a finding (`review.md`
|
|
72
74
|
§Reviewing).
|
|
73
75
|
|
|
74
76
|
**Declared assumptions exist only where no question can be asked:**
|
|
75
77
|
|
|
76
78
|
- **Unattended** — the practitioner is not reachable (the Unattended path above).
|
|
77
|
-
- **Delegated** — their own words handed you this choice;
|
|
79
|
+
- **Delegated** — their own words handed you this choice; cite their entry in the
|
|
80
|
+
request record, or quote them where the unit has none (below L3). A general
|
|
78
81
|
"go ahead" is not a delegation.
|
|
79
82
|
|
|
80
83
|
Each states **what it is taken from** and **the alternative it excludes**, and is
|
|
@@ -170,7 +173,8 @@ round). Cover:
|
|
|
170
173
|
|
|
171
174
|
## Reflect
|
|
172
175
|
|
|
173
|
-
|
|
176
|
+
At L3, append the answers verbatim to the request record first
|
|
177
|
+
(`audit/requests/REQUEST_course_<slug>.md`, template in `templates.md`). Then fold them into the ANALYSIS `## Objective` / `## Vision-Alignment`
|
|
174
178
|
sections (Standalone) or into the D-UC/E-ISP (Hybrid) — do not leave them
|
|
175
179
|
sitting only in the chat transcript. The written document, not the
|
|
176
180
|
conversation, is what the next session and the next reviewer will read.
|
|
@@ -270,6 +270,10 @@ The binding inputs, by review type:
|
|
|
270
270
|
**against** them, not only for internal consistency.
|
|
271
271
|
- **For a closure review, the diff and the test evidence**: the run output that
|
|
272
272
|
backs every completion claim, dated after the final relevant change.
|
|
273
|
+
- **For a design or closure review of an L3/E3 unit, the request record**
|
|
274
|
+
(`audit/requests/REQUEST_[unit].md`): the owner's own words, verbatim, that the
|
|
275
|
+
artifact's owner statements must resolve to. Without it, every such statement is
|
|
276
|
+
a CANNOT VERIFY.
|
|
273
277
|
- **For a design review, the threat model too** (same sources as above). Why this
|
|
274
278
|
one and not the whole set: file coverage crosses the impact-analysis→design hop on
|
|
275
279
|
a mechanical gate (every impacted file needs a design block), so a dropped file is
|
|
@@ -398,7 +402,11 @@ When you are the reviewer:
|
|
|
398
402
|
intent choice written with no owner-authored source and no weighing, a weighing
|
|
399
403
|
that names no rejected option or turns on an owner priority not on record, and a
|
|
400
404
|
fact or owner-held datum settled by a weighing instead of evidence are findings;
|
|
401
|
-
so is a cited source that does not say what the line claims.
|
|
405
|
+
so is a cited source that does not say what the line claims. An owner statement
|
|
406
|
+
the artifact relies on — an answer, an approval, a cost acceptance, a ruling, a
|
|
407
|
+
delegation — that resolves to no entry of the unit's request record, or that the
|
|
408
|
+
record contradicts, is a finding; so is a quotation in the record carrying what
|
|
409
|
+
its template says to redact. What makes a ground
|
|
402
410
|
or an assumption legal is each lens's `elicitation.md` §The question discipline —
|
|
403
411
|
cite it, never restate it. The fix is the question, not a better justification.
|
|
404
412
|
- **Restated facts (cite, never copy).** Every governance slot has ONE owning document
|