diff --git a/AGENTS.md b/AGENTS.md index 522fa51..9152735 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,15 +6,20 @@ instructions for a personal learning space. - `bootstrap.md` gets a nontechnical learner from an agent prompt to a working learning repository. - `pathmx` owns PathMX authoring, tooling, Player use, annotations, and review. -- `path` owns the agent-led personal learning workflow and uses `pathmx` for +- `learn` owns the agent-led personal learning workflow and uses `pathmx` for authoring. +- `teach` owns reusable paths intended for multiple learners and uses `pathmx` + for authoring. +- `pathmx/library/` owns stable reusable patterns, templates, components, and + fictional examples shared by `learn` and `teach`. - `work-log/` keeps design history. It is not installed as skill content. - The public `pathmx-learning-starter` is a consumer, not a source of truth. Keep checked-in content self-contained. Keep prose short and plain. Put core procedure in `SKILL.md`; put detailed syntax and examples in references. -Add only syntax supported by the pinned PathMX version and a local fixture. +Add only syntax supported by the pinned PathMX version and a local fixture or +verified library item. Treat Player interactions, annotations, questions, components, routes, and CLI claims as version-sensitive. Verify them against fixtures or the installed CLI. The exact dependency in `package.json` is the fixture baseline. Keep bootstrap @@ -29,7 +34,8 @@ The learning workflow is buffered, not Block-at-a-time: - adapt at useful session or module boundaries; - keep annotations and durable evidence in Sources. -Keep learner fixtures fictional. Do not add real personal or sensitive data. +Keep learner fixtures and library examples fictional. Do not add real personal +or sensitive data. Test sync changes with temporary repositories, including check mode, conflicts, containment, and rollback. Run `bun run check` before handoff. diff --git a/README.md b/README.md index 3eb45d6..71be04c 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,22 @@ # PathMX Skills -Canonical agent instructions for authoring PathMX and running a personal -learning space. +Canonical agent instructions for authoring PathMX, personal learning, and +shared learning paths. Give an agent [the bootstrap instructions](./bootstrap.md) to create a new -learning repository. After setup, repository instructions and these two skills +learning repository. After setup, repository instructions and these skills carry the workflow: | Skill | Use | | --- | --- | | `/pathmx` | Author, play, review, and verify PathMX. Invoked automatically for PathMX work. | -| `/path` | Start or resume a buffered adaptive learning path for one learner. | +| `/learn` | Start or resume a buffered adaptive learning path for one learner. | +| `/teach` | Design, author, pilot, and review a reusable path for many learners. | + +The `/pathmx` [library](./skills/pathmx/library/index.md) holds stable patterns, +templates, components, and fictional examples shared by `/learn` and `/teach`. +Reusable material is verified in place; diagnostic-only cases stay under +`tests/fixtures/`. Skills install under `.agents/skills/`. Codex discovers that directory directly. Claude Code uses the matching `.claude/skills` discovery link and a @@ -42,8 +48,10 @@ Apply the canonical packages: bun run sync-skills -- --write ``` -Write mode owns only the packages declared in `skills/manifest.json` and their -Claude discovery links. It leaves unrelated target content and skills alone. +Write mode owns the packages declared in `skills/manifest.json`, names they +explicitly replace, and their Claude discovery links. It removes the retired +`/path` package while installing `/learn`, and leaves unrelated target content +and skills alone. ## Evals diff --git a/ROADMAP.md b/ROADMAP.md index fd011c0..e8db65f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -28,11 +28,13 @@ ## Later work -- Forward-test bootstrap and both implicit skill triggers in clean Codex and +- Forward-test bootstrap and all implicit skill triggers in clean Codex and Claude Code sessions. +- Refine `/teach` against representative deployed PathMX paths before treating + its distribution, facilitation, and release conventions as settled. - Expand stable annotation review and reply workflows after field use. -- Add more learning-path examples only when they cover a distinct domain or - learner need. +- Add library examples only when they cover a distinct audience, domain, + authoring pattern, or learner need. - Explore an optional, more playful home and milestone map without making it a dependency of the minimal starter. - Add general actions or spaceholders only after their public authoring diff --git a/bootstrap.md b/bootstrap.md index fa672c6..01216f8 100644 --- a/bootstrap.md +++ b/bootstrap.md @@ -50,7 +50,7 @@ below. ## 3. Create the learning repository Create the chosen directory from the official starter. This command also -installs the current official `/path` and `/pathmx` skills: +installs the current official `/learn` and `/pathmx` skills: ```sh pathmx init --template pathmx-learning-starter @@ -59,7 +59,7 @@ bun install --frozen-lockfile ``` Read `AGENTS.md` and, when present, the instructions for the current agent -harness. Confirm that `.agents/skills/path/SKILL.md` and +harness. Confirm that `.agents/skills/learn/SKILL.md` and `.agents/skills/pathmx/SKILL.md` exist. Shell tool calls may not preserve a prior `cd`. Run every following project @@ -120,7 +120,7 @@ Open the bundled Player tutorial for a first-time learner. They may skip it. ## 5. Begin learning -Use `/path` implicitly. Ask a few questions at a time about: +Use `/learn` implicitly. Ask a few questions at a time about: - what the learner wants to be able to do and why; - what they already know, with one small piece of evidence; diff --git a/scripts/check-markdown-links.test.ts b/scripts/check-markdown-links.test.ts index 0adb17b..e4770a7 100644 --- a/scripts/check-markdown-links.test.ts +++ b/scripts/check-markdown-links.test.ts @@ -24,10 +24,10 @@ async function check(markdown: string) { } describe("Markdown links", () => { - it("accepts valid files, directories, anchors, and query strings", async () => { + it("accepts valid files, directories, anchors, nested labels, and query strings", async () => { expect( await check( - "[file](./docs/target.md) [dir](./docs) [anchor](./docs/target.md?view=1#target-heading)\n", + "[:lucide-arrow-right[Next]:](./docs/target.md) [file](./docs/target.md) [dir](./docs) [anchor](./docs/target.md?view=1#target-heading)\n", ), ).toEqual([]) }) diff --git a/scripts/check-markdown-links.ts b/scripts/check-markdown-links.ts index dd3f943..5646916 100644 --- a/scripts/check-markdown-links.ts +++ b/scripts/check-markdown-links.ts @@ -55,7 +55,9 @@ function maskIgnoredMarkdown(markdown: string) { export function extractMarkdownLinks(markdown: string): MarkdownLink[] { const links: MarkdownLink[] = [] for (const [index, line] of maskIgnoredMarkdown(markdown).entries()) { - for (const match of line.matchAll(/!?\[[^\]]*\]\((<[^>]+>|[^)]+)\)/g)) { + for (const match of line.matchAll( + /!?\[(?:[^\[\]]|\[[^\[\]]*\])*\]\((<[^>]+>|[^)]+)\)/g, + )) { let target = match[1]?.trim() ?? "" if (target.startsWith("<") && target.endsWith(">")) { target = target.slice(1, -1) @@ -65,7 +67,9 @@ export function extractMarkdownLinks(markdown: string): MarkdownLink[] { if (target) links.push({ line: index + 1, target }) } - const definition = line.match(/^\s*\[(?!\^)[^\]]+\]:\s*(<[^>]+>|\S+)/) + const definition = line.match( + /^\s*\[(?!\^)[^\]]+\]:\s*(?!\]\()(<[^>]+>|\S+)/, + ) if (definition?.[1]) { links.push({ line: index + 1, diff --git a/scripts/check-pathmx-docs.ts b/scripts/check-pathmx-docs.ts index 572bdc5..c114302 100644 --- a/scripts/check-pathmx-docs.ts +++ b/scripts/check-pathmx-docs.ts @@ -49,6 +49,7 @@ const referenceEntries = [ "skills/pathmx/references/pathmx-questions.md", "skills/pathmx/references/pathmx-annotations.md", "skills/pathmx/references/pathmx-literate-components.md", + "skills/pathmx/references/pathmx-icons.md", "skills/pathmx/references/pathmx-code.md", "skills/pathmx/references/pathmx-math.md", "skills/pathmx/references/pathmx-media.md", @@ -97,6 +98,32 @@ async function build( return { outputDir, paths, stdout, sourcePaths } } +async function requireBuildFails( + cwd: string, + outputDir: string, + entries: string[], + expectedDiagnostics: string[], +) { + const child = Bun.spawn( + [pathmxBin, "build", ...entries, "-o", outputDir, "--clean"], + { cwd, stdout: "pipe", stderr: "pipe" }, + ) + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]) + const diagnostics = `${stdout}\n${stderr}`.trim() + if (exitCode === 0) { + throw new Error(`PathMX unexpectedly accepted an invalid fixture:\n${diagnostics}`) + } + for (const expected of expectedDiagnostics) { + if (!diagnostics.includes(expected)) { + throw new Error(`PathMX failure missing diagnostic ${expected}:\n${diagnostics}`) + } + } +} + function requireEntries(result: BuildResult, entries: string[]) { const builtEntries = new Set(Object.values(result.paths.paths).map((entry) => entry.entry)) for (const entry of entries) { @@ -189,6 +216,71 @@ export async function checkPathmxDocs() { throw new Error("Literate Component example did not expand") } + const iconsOutput = await outputForEntry( + references, + "skills/pathmx/references/pathmx-icons.md", + ) + const iconsHtml = await readFile( + path.join(iconsOutput, "skills", "pathmx", "references", "pathmx-icons.html"), + "utf8", + ) + for (const expected of [ + 'data-pathmx-icon="lucide:pen-tool"', + 'data-pathmx-icon="lucide:sparkles"', + 'data-pathmx-icon="lucide:arrow-right"', + 'data-pathmx-icon="lucide:book-open"', + 'aria-label="Continue to icon choices"', + 'data-pathmx-icons="lucide"', + 'class="pmx-icon__svg"', + ]) { + if (!iconsHtml.includes(expected)) { + throw new Error(`Lucide icon reference missing rendered evidence: ${expected}`) + } + } + if ((iconsHtml.match(/data-pathmx-icon="lucide:/g) ?? []).length !== 4) { + throw new Error("Lucide icon reference transformed code examples") + } + if ( + !/class="pmx-icon"[^>]+data-pathmx-icon="lucide:sparkles"[^>]+aria-hidden="true"/.test( + iconsHtml, + ) + ) { + throw new Error("Decorative Lucide icon is not hidden from assistive technology") + } + if ( + !/class="pmx-icon"[^>]+data-pathmx-icon="lucide:arrow-right"[^>]+role="img"[^>]+aria-label="Continue to icon choices"/.test( + iconsHtml, + ) + ) { + throw new Error("Labeled Lucide icon is missing its accessible image contract") + } + + const iconFailureRoot = path.join(repoRoot, "tests", "fixtures", "pathmx", "icons") + const iconFailureOutput = path.join(tempRoot, "icon-failure") + await requireBuildFails( + iconFailureRoot, + iconFailureOutput, + ["unknown.path.md"], + [ + "icons-lucide/unknown-icon", + 'Unknown Lucide icon "definitely-not-real"', + ], + ) + const iconFailurePaths = await readJson( + path.join(iconFailureOutput, "paths.json"), + ) + const iconFailurePath = Object.values(iconFailurePaths.paths).find( + (entry) => entry.entry === "unknown.path.md", + ) + if (!iconFailurePath) throw new Error("Unknown icon fixture did not emit a Path") + const iconFailureHtml = await readFile( + path.join(iconFailureOutput, iconFailurePath.outputPath, "unknown.path.html"), + "utf8", + ) + if (!iconFailureHtml.includes(":lucide-definitely-not-real:")) { + throw new Error("Unknown icon fixture did not preserve fallback text") + } + const coreRoot = path.join(repoRoot, "tests", "fixtures", "pathmx", "core") const core = await build(coreRoot, path.join(tempRoot, "core"), ["index.path.md"]) requireEntries(core, ["index.path.md"]) @@ -201,6 +293,121 @@ export async function checkPathmxDocs() { throw new Error("Core include or component fixture did not render") } + const libraryComponentRoot = path.join( + repoRoot, + "skills", + "pathmx", + "library", + ) + const libraryComponents = await build( + libraryComponentRoot, + path.join(tempRoot, "library-components"), + ["examples/shared-components/index.path.md"], + ) + requireEntries(libraryComponents, ["examples/shared-components/index.path.md"]) + if (!libraryComponents.sourcePaths.has("components/feedback-panel.components.md")) { + throw new Error("Library feedback panel component was not included") + } + const libraryComponentOutput = await outputForEntry( + libraryComponents, + "examples/shared-components/index.path.md", + ) + const libraryComponentHtml = await readFile( + path.join(libraryComponentOutput, "examples", "shared-components", "index.path.html"), + "utf8", + ) + if (!libraryComponentHtml.includes('class="feedback-panel"')) { + throw new Error("Library feedback panel component did not render") + } + + const learnTemplateRoot = path.join( + repoRoot, + "skills", + "pathmx", + "library", + "templates", + "learn", + ) + const learnTemplates = await build( + learnTemplateRoot, + path.join(tempRoot, "learn-templates"), + ["path/index.path.md", "module/index.path.md"], + ) + requireEntries(learnTemplates, ["path/index.path.md", "module/index.path.md"]) + + const teachTemplateRoot = path.join( + repoRoot, + "skills", + "pathmx", + "library", + "templates", + "teach", + ) + const teachTemplates = await build( + teachTemplateRoot, + path.join(tempRoot, "teach-templates"), + ["path/index.path.md", "module/index.path.md"], + ) + requireEntries(teachTemplates, ["path/index.path.md", "module/index.path.md"]) + + const stylingRoot = path.join(repoRoot, "tests", "fixtures", "pathmx", "styling") + const styling = await build(stylingRoot, path.join(tempRoot, "styling"), [ + "index.path.md", + ]) + requireEntries(styling, ["index.path.md"]) + for (const source of ["unthemed.path.md", "themed.path.md"]) { + if (!styling.sourcePaths.has(source)) { + throw new Error(`Styling fixture missing ${source}`) + } + } + const stylingOutput = await outputForEntry(styling, "index.path.md") + const unthemedHtml = await readFile( + path.join(stylingOutput, "unthemed.path.html"), + "utf8", + ) + const themedHtml = await readFile( + path.join(stylingOutput, "themed.path.html"), + "utf8", + ) + for (const html of [unthemedHtml, themedHtml]) { + if ( + !html.includes('data-pathmx-style="root"') || + !html.includes('data-pathmx-style-source="base.css"') + ) { + throw new Error("Styling fixture missing the graph root stylesheet") + } + } + if (unthemedHtml.includes("data-pathmx-theme-source")) { + throw new Error("Unthemed styling fixture unexpectedly emitted a Source theme") + } + for (const expected of [ + 'data-pathmx-theme-source="themed.path"', + "--pmx-color-accent: #c2410c;", + 'data-pathmx-style-name="lab"', + 'data-pathmx-style-source="lab.css"', + ]) { + if (!themedHtml.includes(expected)) { + throw new Error(`Themed styling fixture missing: ${expected}`) + } + } + if ( + themedHtml.indexOf('data-pathmx-style="root"') > + themedHtml.indexOf('data-pathmx-style-name="lab"') + ) { + throw new Error("Local styling fixture must load after the root stylesheet") + } + const baseHref = unthemedHtml.match( + /href="([^"]+)"[^>]+data-pathmx-style="root"[^>]+data-pathmx-style-source="base\.css"/, + )?.[1] + if (!baseHref) throw new Error("Styling fixture missing the root stylesheet asset") + const baseCss = await readFile( + path.join(stylingOutput, baseHref.replace(/^\//, "")), + "utf8", + ) + if (!baseCss.includes("--fixture-base-theme: active")) { + throw new Error("Styling fixture root stylesheet asset is incomplete") + } + const configRoot = path.join(repoRoot, "tests", "fixtures", "pathmx", "config") const config = await build(configRoot, path.join(tempRoot, "config"), []) requireEntries(config, ["index.path.md", "workshop.path.md"]) @@ -287,8 +494,15 @@ export async function checkPathmxDocs() { if (!example.sourcePaths.has(source)) throw new Error(`Repo example missing ${source}`) } - const pathRoot = path.join(repoRoot, "tests", "fixtures", "path") - const personalPath = await build(pathRoot, path.join(tempRoot, "path"), [ + const learnExampleRoot = path.join( + repoRoot, + "skills", + "pathmx", + "library", + "examples", + "learn-sql-foundations", + ) + const personalPath = await build(learnExampleRoot, path.join(tempRoot, "learn"), [ "paths/sql-foundations/index.path.md", ]) requireEntries(personalPath, ["sql-foundations/index.path.md"]) @@ -305,7 +519,7 @@ export async function checkPathmxDocs() { ] for (const source of expectedPathSources) { if (!personalPath.sourcePaths.has(source)) { - throw new Error(`Personal path fixture missing ${source}`) + throw new Error(`Learn library example missing ${source}`) } } @@ -324,7 +538,7 @@ export async function checkPathmxDocs() { "utf8", ) if (!personalAssessmentHtml.includes("Explain why the other join would drop rows")) { - throw new Error("Personal path checkpoint missing authored prompt") + throw new Error("Learn library checkpoint missing authored prompt") } const personalGraph = await readJson( path.join(personalOutput, "graph-index.json"), @@ -341,13 +555,16 @@ export async function checkPathmxDocs() { explanation?.props?.actions?.submit !== "questions.submitText" || explanation.props.question?.type !== "long" ) { - throw new Error("Personal path checkpoint missing question graph contract") + throw new Error("Learn library checkpoint missing question graph contract") } return { paths: Object.keys(references.paths.paths).length + Object.keys(core.paths.paths).length + + Object.keys(libraryComponents.paths.paths).length + + Object.keys(learnTemplates.paths.paths).length + + Object.keys(teachTemplates.paths.paths).length + Object.keys(config.paths.paths).length + Object.keys(questions.paths.paths).length + Object.keys(annotations.paths.paths).length + @@ -356,6 +573,9 @@ export async function checkPathmxDocs() { sources: new Set([ ...references.sourcePaths, ...core.sourcePaths, + ...libraryComponents.sourcePaths, + ...learnTemplates.sourcePaths, + ...teachTemplates.sourcePaths, ...config.sourcePaths, ...questions.sourcePaths, ...annotations.sourcePaths, diff --git a/scripts/check-self-contained.test.ts b/scripts/check-self-contained.test.ts index 3ebd356..df9fc5a 100644 --- a/scripts/check-self-contained.test.ts +++ b/scripts/check-self-contained.test.ts @@ -19,7 +19,7 @@ describe("self-containment checks", () => { it("allows internal parent links", () => { expect( inspectSelfContainedText( - "skills/path/references/example.md", + "skills/learn/references/example.md", "[Skill](../SKILL.md)", root, ), diff --git a/scripts/check-skill-packages.ts b/scripts/check-skill-packages.ts index 0e048f1..451dedf 100644 --- a/scripts/check-skill-packages.ts +++ b/scripts/check-skill-packages.ts @@ -18,6 +18,7 @@ type SkillManifest = { invocation: "automatic-and-explicit" | "explicit" purpose: string dependsOn: string[] + replaces?: string[] }> } @@ -118,12 +119,22 @@ export async function validateSkillPackages(repoRoot: string) { findings.push({ file: "skills/manifest.json", message: "unsupported schema or version" }) } const manifestNames = manifest.skills.map((skill) => skill.name).sort() + const replacedNames = manifest.skills.flatMap((skill) => skill.replaces ?? []) if (JSON.stringify(manifestNames) !== JSON.stringify(names)) { findings.push({ file: "skills/manifest.json", message: `manifest skills (${manifestNames.join(", ")}) must match directories (${names.join(", ")})`, }) } + if ( + new Set(replacedNames).size !== replacedNames.length || + replacedNames.some((name) => manifestNames.includes(name)) + ) { + findings.push({ + file: "skills/manifest.json", + message: "replaced skill names must be unique and retired", + }) + } for (const skill of manifest.skills) { if (skill.directory !== `skills/${skill.name}`) { findings.push({ @@ -137,6 +148,12 @@ export async function validateSkillPackages(repoRoot: string) { message: `invalid metadata for ${skill.name}`, }) } + if (skill.replaces && !Array.isArray(skill.replaces)) { + findings.push({ + file: "skills/manifest.json", + message: `invalid replacements for ${skill.name}`, + }) + } for (const dependency of skill.dependsOn) { if (!manifestNames.includes(dependency)) { findings.push({ diff --git a/scripts/evals.test.ts b/scripts/evals.test.ts index b05359e..53c2cbf 100644 --- a/scripts/evals.test.ts +++ b/scripts/evals.test.ts @@ -194,13 +194,13 @@ describe("deterministic grader", () => { temporaryRoots.push(root) for (const directory of [ ".git", - ".agents/skills/path", + ".agents/skills/learn", ".agents/skills/pathmx", "paths/sql/modules/01-basics", ]) { await mkdir(path.join(root, directory), { recursive: true }) } - await writeFile(path.join(root, ".agents/skills/path/SKILL.md"), "path") + await writeFile(path.join(root, ".agents/skills/learn/SKILL.md"), "learn") await writeFile(path.join(root, ".agents/skills/pathmx/SKILL.md"), "pathmx") await writeFile( path.join(root, "package.json"), diff --git a/scripts/evals/core.ts b/scripts/evals/core.ts index 77eec52..7201052 100644 --- a/scripts/evals/core.ts +++ b/scripts/evals/core.ts @@ -562,13 +562,13 @@ export async function gradeLearningSpace( { critical: true, weight: 2 }, ) - const skills = ["path", "pathmx"].filter((name) => + const skills = ["learn", "pathmx"].filter((name) => existsSync(path.join(workspace, ".agents", "skills", name, "SKILL.md")), ) addCheck( checks, "skills.installed", - "Path and PathMX skills installed", + "Learn and PathMX skills installed", skills.length === 2, skills.join(", ") || "none", { critical: true, weight: 3 }, diff --git a/scripts/learn-skill.test.ts b/scripts/learn-skill.test.ts new file mode 100644 index 0000000..fcdb5dd --- /dev/null +++ b/scripts/learn-skill.test.ts @@ -0,0 +1,211 @@ +import { describe, expect, it } from "bun:test" +import { readFile } from "node:fs/promises" +import path from "node:path" +import { parse } from "yaml" + +const repoRoot = path.resolve(import.meta.dir, "..") + +async function read(relative: string) { + return readFile(path.join(repoRoot, relative), "utf8") +} + +describe("learn and teach skill contracts", () => { + it("pins one machine-readable PathMX compatibility baseline", async () => { + const packageJson = JSON.parse(await read("package.json")) + const baseline = packageJson.pathmxCompatibility.baseline + expect(baseline).toBe(packageJson.devDependencies["@fellowhumans/pathmx"]) + expect(packageJson.pathmxCompatibility.updatePolicy).toBe( + "latest-after-verification", + ) + expect(baseline).toMatch(/^\d+\.\d+\.\d+$/) + }) + + it("supports implicit and explicit learn invocation", async () => { + const skill = await read("skills/learn/SKILL.md") + const frontmatter = parse(skill.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "") + const interfaceConfig = parse(await read("skills/learn/agents/openai.yaml")) + const manifest = JSON.parse(await read("skills/manifest.json")) + const declared = manifest.skills.find((entry: { name: string }) => entry.name === "learn") + + expect(frontmatter.name).toBe("learn") + expect(frontmatter.description).toMatch(/automatically/) + expect(interfaceConfig.interface.default_prompt).toContain("$learn") + expect(interfaceConfig.policy?.allow_implicit_invocation).not.toBe(false) + expect(declared.invocation).toBe("automatic-and-explicit") + }) + + it("uses a buffered module instead of Block-at-a-time generation", async () => { + const skill = await read("skills/learn/SKILL.md") + for (const term of [ + "3–7 milestones", + "2–4 sessions", + "current module", + "worked example", + "without waiting", + "module checkpoint", + "annotations", + ]) { + expect(skill).toContain(term) + } + expect(skill).not.toMatch(/one agent turn (?:adds|=) one Block/i) + expect(skill).not.toMatch(/no core concept missed before every/i) + }) + + it("uses bounded orchestration without moving the learner loop to workers", async () => { + const skill = await read("skills/learn/SKILL.md") + const loop = await read("skills/learn/references/buffered-loop.md") + + expect(skill).toMatch(/Do not draft\s+session Sources before confirmation/) + expect(skill).toMatch(/one owner\s+per file/) + expect(skill).toContain("The parent agent owns the learner conversation") + expect(skill).toContain("Never wait for optional worker output") + expect(loop).toContain("## Fast orchestration lane") + expect(loop).toContain("Workers return concise drafts or findings") + expect(loop).toMatch(/do not draft session Sources/) + expect(loop).toContain("Do not allow nested delegation") + expect(loop).toMatch(/shared\s+terminology/) + expect(loop).toMatch(/parent runs one full check/) + expect(loop).toContain("Set a join point before learner handoff") + expect(loop).toMatch(/continue in\s+the parent/) + expect(`${skill}\n${loop}`).not.toContain("context: fork") + }) + + it("ships a progressive map artifact before the buffered module", async () => { + const skill = await read("skills/learn/SKILL.md") + const map = await read("skills/pathmx/library/templates/learn/path/index.path.md") + expect(skill).toMatch(/before showing it to the\s+learner/) + expect(skill).toContain("Do not create session, review, or checkpoint Sources") + expect(map).toContain("# Proposed learning path") + expect(map.match(/\b(?:ready|planned):?\b/g)?.length ?? 0).toBeGreaterThanOrEqual(3) + expect(map).toContain("Evidence:") + }) + + it("keeps evidence, progress, and history durable", async () => { + const skill = await read("skills/learn/SKILL.md") + for (const term of [ + "Point A", + "Point B", + "evidence targets", + "learning.activity.md", + "append-only", + "foreground path", + ]) { + expect(skill).toContain(term) + } + }) + + it("personalizes presentation without replacing the learning structure", async () => { + const skill = await read("skills/learn/SKILL.md") + expect(skill).toContain("visual mood") + expect(skill).toContain("theme tokens") + expect(skill).toContain("reduced motion") + expect(skill).toContain("Keep navigation and learning structure stable") + }) + + it("owns Player uptime, exact routes, and browser fallback", async () => { + const skill = await read("skills/learn/SKILL.md") + const pathmx = await read("skills/pathmx/SKILL.md") + for (const content of [skill, pathmx]) { + expect(content).toContain("pathmx route") + expect(content).toContain("@Browser") + expect(content).toMatch(/system browser|clickable/i) + } + }) + + it("ships the buffered loop and complete library example", async () => { + const loop = await read("skills/learn/references/buffered-loop.md") + const exampleModule = await read( + "skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/modules/01-combine-tables/index.path.md", + ) + expect(loop).toContain("Planning horizon") + expect(loop).toContain("One module") + expect(loop).toContain("Do not use a no-core-miss gate after every session") + expect(exampleModule).toContain("Both sessions are ready") + expect(exampleModule).toContain("Milestone checkpoint") + }) + + it("separates personal learning from reusable teaching paths", async () => { + const learn = await read("skills/learn/SKILL.md") + const teach = await read("skills/teach/SKILL.md") + const teachContract = await read("skills/teach/references/shared-path-contract.md") + const teachFrontmatter = parse( + teach.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "", + ) + const manifest = JSON.parse(await read("skills/manifest.json")) + const declared = manifest.skills.find((entry: { name: string }) => entry.name === "teach") + + expect(learn).toContain("one learner") + expect(teachFrontmatter.name).toBe("teach") + expect(teachFrontmatter.description).toContain("multiple learners") + expect(teach).toContain("Do not make a personal") + expect(teachContract).toContain("Pilot-ready") + expect(declared.dependsOn).toEqual(["pathmx"]) + }) + + it("provides one shared verified library", async () => { + const library = await read("skills/pathmx/library/index.md") + const learn = await read("skills/learn/SKILL.md") + const teach = await read("skills/teach/SKILL.md") + const component = await read( + "skills/pathmx/library/components/feedback-panel.components.md", + ) + + for (const kind of ["Pattern", "Template", "Component", "Example"]) { + expect(library).toContain(kind) + } + expect(library).toContain("pinned PathMX baseline") + expect(learn).toContain("../pathmx/library/index.md") + expect(teach).toContain("../pathmx/library/index.md") + expect(component).toContain("componentName: feedback-panel") + }) + + it("documents the verified Lucide icon contract", async () => { + const skill = await read("skills/pathmx/SKILL.md") + const icons = await read("skills/pathmx/references/pathmx-icons.md") + + expect(skill).toContain("[Lucide icons](./references/pathmx-icons.md)") + expect(icons).toContain(":lucide-sparkles:") + expect(icons).toContain(":lucide-info[Information]:") + expect(icons).toContain("assistive technology") + expect(icons).toContain("unknown name is a build error") + expect(icons).toContain(".pmx-icon__svg") + }) + + it("documents and fixtures authored Source and Block style classes", async () => { + const styling = await read("skills/pathmx/references/pathmx-styling.md") + const fixture = await read("tests/fixtures/pathmx/styling/themed.path.md") + + expect(styling).toContain("styles.classes") + expect(styling).toContain(".pmx-document.landing-page") + expect(styling).toContain(".pmx-block.pmx-prose.page-header.full-bleed") + expect(styling).toContain("`pmx-` prefix is reserved") + expect(styling).toContain("prose and feature baselines at zero specificity") + expect(fixture).toContain("classes: [technical-lab]") + expect(fixture).toContain("classes: [callout, full-bleed, callout]") + }) + + it("keeps Tram's contribution visible and specific", async () => { + const readme = await read("README.md") + expect(readme).toContain("Tram Le") + expect(readme).toContain("early hands-on testing") + expect(readme).toContain("math, media, code, tooling") + }) + + it("provides a one-file bootstrap", async () => { + const bootstrap = await read("bootstrap.md") + for (const term of [ + "bun --version", + "pathmx self-update", + "@fellowhumans/pathmx@latest", + "pathmx-learning-starter", + ".agents/skills/learn/SKILL.md", + "git init", + "bun run play", + "bun run check:candidate", + "Player tutorial", + "git restore package.json bun.lock", + ]) { + expect(bootstrap).toContain(term) + } + }) +}) diff --git a/scripts/path-skill.test.ts b/scripts/path-skill.test.ts deleted file mode 100644 index 2a5b087..0000000 --- a/scripts/path-skill.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { describe, expect, it } from "bun:test" -import { readFile } from "node:fs/promises" -import path from "node:path" -import { parse } from "yaml" - -const repoRoot = path.resolve(import.meta.dir, "..") - -async function read(relative: string) { - return readFile(path.join(repoRoot, relative), "utf8") -} - -describe("path skill contract", () => { - it("pins one machine-readable PathMX compatibility baseline", async () => { - const packageJson = JSON.parse(await read("package.json")) - const baseline = packageJson.pathmxCompatibility.baseline - expect(baseline).toBe(packageJson.devDependencies["@fellowhumans/pathmx"]) - expect(packageJson.pathmxCompatibility.updatePolicy).toBe( - "latest-after-verification", - ) - expect(baseline).toMatch(/^\d+\.\d+\.\d+$/) - }) - - it("supports implicit and explicit invocation", async () => { - const skill = await read("skills/path/SKILL.md") - const frontmatter = parse(skill.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "") - const interfaceConfig = parse(await read("skills/path/agents/openai.yaml")) - const manifest = JSON.parse(await read("skills/manifest.json")) - const declared = manifest.skills.find((entry: { name: string }) => entry.name === "path") - - expect(frontmatter.name).toBe("path") - expect(frontmatter.description).toMatch(/automatically/) - expect(interfaceConfig.policy?.allow_implicit_invocation).not.toBe(false) - expect(declared.invocation).toBe("automatic-and-explicit") - }) - - it("uses a buffered module instead of Block-at-a-time generation", async () => { - const skill = await read("skills/path/SKILL.md") - for (const term of [ - "3–7 milestones", - "2–4 sessions", - "current module", - "worked example", - "without waiting", - "module checkpoint", - "annotations", - ]) { - expect(skill).toContain(term) - } - expect(skill).not.toMatch(/one agent turn (?:adds|=) one Block/i) - expect(skill).not.toMatch(/no core concept missed before every/i) - }) - - it("ships a progressive map artifact before the buffered module", async () => { - const skill = await read("skills/path/SKILL.md") - const map = await read("skills/path/assets/path/index.path.md") - expect(skill).toMatch(/before showing it to the\s+learner/) - expect(skill).toContain("Do not create session, review, or checkpoint Sources") - expect(map).toContain("# Proposed learning path") - expect(map.match(/\b(?:ready|planned):?\b/g)?.length ?? 0).toBeGreaterThanOrEqual(3) - expect(map).toContain("Evidence:") - }) - - it("keeps evidence, progress, and history durable", async () => { - const skill = await read("skills/path/SKILL.md") - for (const term of [ - "Point A", - "Point B", - "evidence targets", - "learning.activity.md", - "append-only", - "foreground path", - ]) { - expect(skill).toContain(term) - } - }) - - it("personalizes presentation without replacing the learning structure", async () => { - const skill = await read("skills/path/SKILL.md") - expect(skill).toContain("visual mood") - expect(skill).toContain("theme tokens") - expect(skill).toContain("reduced motion") - expect(skill).toContain("Keep navigation and learning structure stable") - }) - - it("owns Player uptime, exact routes, and browser fallback", async () => { - const skill = await read("skills/path/SKILL.md") - const pathmx = await read("skills/pathmx/SKILL.md") - for (const content of [skill, pathmx]) { - expect(content).toContain("pathmx route") - expect(content).toContain("@Browser") - expect(content).toMatch(/system browser|clickable/i) - } - }) - - it("ships self-contained loop and worked references", async () => { - const loop = await read("skills/path/references/buffered-loop.md") - const example = await read("skills/path/references/worked-example.md") - expect(loop).toContain("Planning horizon") - expect(loop).toContain("One module") - expect(loop).toContain("Do not use a no-core-miss gate after every session") - expect(example).toContain("Both sessions exist before the learner starts") - expect(example).toContain("Milestone checkpoint") - }) - - it("keeps Tram's contribution visible and specific", async () => { - const readme = await read("README.md") - expect(readme).toContain("Tram Le") - expect(readme).toContain("early hands-on testing") - expect(readme).toContain("math, media, code, tooling") - }) - - it("provides a one-file bootstrap", async () => { - const bootstrap = await read("bootstrap.md") - for (const term of [ - "bun --version", - "pathmx self-update", - "@fellowhumans/pathmx@latest", - "pathmx-learning-starter", - "git init", - "bun run play", - "bun run check:candidate", - "Player tutorial", - "git restore package.json bun.lock", - ]) { - expect(bootstrap).toContain(term) - } - }) -}) diff --git a/scripts/sync-skills.test.ts b/scripts/sync-skills.test.ts index 5363084..7e33e38 100644 --- a/scripts/sync-skills.test.ts +++ b/scripts/sync-skills.test.ts @@ -38,7 +38,7 @@ async function temp(prefix: string) { async function createCanonical() { const root = await temp("pathmx-canonical-") const skills = path.join(root, "skills") - for (const name of ["pathmx", "path"]) { + for (const name of ["pathmx", "learn", "teach"]) { await mkdir(path.join(skills, name, "references"), { recursive: true }) await writeFile( path.join(skills, name, "SKILL.md"), @@ -49,7 +49,13 @@ async function createCanonical() { const manifest = path.join(skills, "manifest.json") await writeFile( manifest, - JSON.stringify({ skills: [{ name: "pathmx" }, { name: "path" }] }), + JSON.stringify({ + skills: [ + { name: "pathmx" }, + { name: "learn", replaces: ["path"] }, + { name: "teach" }, + ], + }), ) return { root, skills, manifest } } @@ -69,8 +75,9 @@ describe("skill sync", () => { it("discovers declared skills in sorted order", async () => { const canonical = await createCanonical() expect(await discoverSkills(canonical.skills, canonical.manifest)).toEqual([ - "path", + "learn", "pathmx", + "teach", ]) }) @@ -97,17 +104,17 @@ describe("skill sync", () => { const target = await createTarget() const layout = await layoutFor(target, canonical) await writeSkills(layout, canonical.skills) - await writeFile(path.join(layout.agentSkillsDir, "path", "SKILL.md"), "changed\n") - await rm(path.join(layout.agentSkillsDir, "path", "references", "note.md")) - await writeFile(path.join(layout.agentSkillsDir, "path", "extra.md"), "extra\n") + await writeFile(path.join(layout.agentSkillsDir, "learn", "SKILL.md"), "changed\n") + await rm(path.join(layout.agentSkillsDir, "learn", "references", "note.md")) + await writeFile(path.join(layout.agentSkillsDir, "learn", "extra.md"), "extra\n") expect(await compareSkill( - path.join(canonical.skills, "path"), - path.join(layout.agentSkillsDir, "path"), - "path", + path.join(canonical.skills, "learn"), + path.join(layout.agentSkillsDir, "learn"), + "learn", )).toEqual([ - { kind: "extra", path: "path/extra.md" }, - { kind: "missing", path: "path/references/note.md" }, - { kind: "changed", path: "path/SKILL.md" }, + { kind: "extra", path: "learn/extra.md" }, + { kind: "missing", path: "learn/references/note.md" }, + { kind: "changed", path: "learn/SKILL.md" }, ]) }) @@ -123,6 +130,42 @@ describe("skill sync", () => { ) }) + it("removes the retired path skill while installing learn", async () => { + const canonical = await createCanonical() + const target = await createTarget(true) + await mkdir(path.join(target, ".agents", "skills", "path"), { recursive: true }) + await writeFile( + path.join(target, ".agents", "skills", "path", "SKILL.md"), + "old-path\n", + ) + await mkdir(path.join(target, ".claude", "skills"), { recursive: true }) + await symlink( + ["..", "..", ".agents", "skills", "path"].join("/"), + path.join(target, ".claude", "skills", "path"), + ) + + const layout = await layoutFor(target, canonical) + expect((await inspectDrift(layout, canonical.skills)).files).toContainEqual({ + kind: "extra", + path: "path (retired)", + }) + expect((await inspectDrift(layout, canonical.skills)).links).toContain( + `extra .claude/skills/path -> ${[ + "..", + "..", + ".agents", + "skills", + "path", + ].join("/")}`, + ) + + await writeSkills(layout, canonical.skills) + await expect(lstat(path.join(layout.agentSkillsDir, "path"))).rejects.toThrow() + await expect(lstat(path.join(layout.claudeSkills, "path"))).rejects.toThrow() + expect(await readFile(path.join(layout.agentSkillsDir, "learn", "SKILL.md"), "utf8")) + .toContain("name: learn") + }) + it("uses per-skill links when Claude skills is a real directory", async () => { const canonical = await createCanonical() const target = await createTarget(true) @@ -130,8 +173,8 @@ describe("skill sync", () => { await writeFile(path.join(target, ".claude", "skills", "local.txt"), "keep\n") const layout = await layoutFor(target, canonical) await writeSkills(layout, canonical.skills) - expect(await readlink(path.join(layout.claudeSkills, "path"))).toBe( - ["..", "..", ".agents", "skills", "path"].join("/"), + expect(await readlink(path.join(layout.claudeSkills, "learn"))).toBe( + ["..", "..", ".agents", "skills", "learn"].join("/"), ) expect(await readFile(path.join(layout.claudeSkills, "local.txt"), "utf8")).toBe("keep\n") }) @@ -139,7 +182,7 @@ describe("skill sync", () => { it("rejects a real per-skill Claude conflict before writing", async () => { const canonical = await createCanonical() const target = await createTarget() - await mkdir(path.join(target, ".claude", "skills", "path"), { recursive: true }) + await mkdir(path.join(target, ".claude", "skills", "learn"), { recursive: true }) await expect(layoutFor(target, canonical)).rejects.toThrow("Claude skill conflict") await expect(lstat(path.join(target, ".agents"))).rejects.toThrow() }) @@ -161,10 +204,12 @@ describe("skill sync", () => { it("restores all managed skills after a mid-transaction failure", async () => { const canonical = await createCanonical() const target = await createTarget() - for (const name of ["path", "pathmx"]) { + for (const name of ["learn", "pathmx", "teach"]) { await mkdir(path.join(target, ".agents", "skills", name), { recursive: true }) await writeFile(path.join(target, ".agents", "skills", name, "SKILL.md"), `old-${name}\n`) } + await mkdir(path.join(target, ".agents", "skills", "path"), { recursive: true }) + await writeFile(path.join(target, ".agents", "skills", "path", "SKILL.md"), "old-path\n") const layout = await layoutFor(target, canonical) await expect( writeSkills(layout, canonical.skills, { @@ -173,12 +218,18 @@ describe("skill sync", () => { }, }), ).rejects.toThrow("install failure") - expect(await readFile(path.join(layout.agentSkillsDir, "path", "SKILL.md"), "utf8")).toBe( - "old-path\n", + expect(await readFile(path.join(layout.agentSkillsDir, "learn", "SKILL.md"), "utf8")).toBe( + "old-learn\n", ) expect(await readFile(path.join(layout.agentSkillsDir, "pathmx", "SKILL.md"), "utf8")).toBe( "old-pathmx\n", ) + expect(await readFile(path.join(layout.agentSkillsDir, "teach", "SKILL.md"), "utf8")).toBe( + "old-teach\n", + ) + expect(await readFile(path.join(layout.agentSkillsDir, "path", "SKILL.md"), "utf8")).toBe( + "old-path\n", + ) }) it("rejects symlinked parents without touching the outside target", async () => { diff --git a/scripts/sync-skills.ts b/scripts/sync-skills.ts index 98b32e8..f6aabd4 100644 --- a/scripts/sync-skills.ts +++ b/scripts/sync-skills.ts @@ -37,6 +37,7 @@ export type TargetLayout = { claudeSkills: string claudeMode: "root-link" | "per-skill-links" skills: string[] + retiredSkills: string[] } export type SyncHooks = { @@ -82,6 +83,25 @@ export async function discoverSkills( return names } +async function discoverRetiredSkills(declaredManifest = manifestPath) { + const manifest = JSON.parse(await readFile(declaredManifest, "utf8")) as { + skills?: Array<{ name?: string; replaces?: string[] }> + } + const current = new Set( + (manifest.skills ?? []) + .map((skill) => skill.name) + .filter((name): name is string => typeof name === "string"), + ) + const retired = (manifest.skills ?? []).flatMap((skill) => skill.replaces ?? []) + if (retired.some((name) => current.has(name))) { + throw new Error("Replaced skill names must not match current packages") + } + if (new Set(retired).size !== retired.length) { + throw new Error("Replaced skill names must be unique") + } + return retired.sort() +} + export async function listFiles(root: string): Promise { const rootStats = await stats(root) if (!rootStats) return [] @@ -183,6 +203,7 @@ export async function inspectTarget( } const skills = await discoverSkills(canonicalSkills, canonicalManifest) + const retiredSkills = await discoverRetiredSkills(canonicalManifest) const agentDir = path.join(root, ".agents") const agentSkillsDir = path.join(agentDir, "skills") const claudeDir = path.join(root, ".claude") @@ -201,7 +222,7 @@ export async function inspectTarget( throw new Error(`.claude/skills must be a directory or symlink: ${claudeSkills}`) } claudeMode = "per-skill-links" - for (const name of skills) { + for (const name of [...skills, ...retiredSkills]) { const link = path.join(claudeSkills, name) assertInside(root, link) const linkStats = await stats(link) @@ -211,7 +232,7 @@ export async function inspectTarget( } } - for (const name of skills) { + for (const name of [...skills, ...retiredSkills]) { assertInside(root, path.join(agentSkillsDir, name)) } @@ -223,6 +244,7 @@ export async function inspectTarget( claudeSkills, claudeMode, skills, + retiredSkills, } } @@ -237,6 +259,11 @@ export async function inspectDrift(layout: TargetLayout, canonicalSkills = skill )), ) } + for (const name of layout.retiredSkills) { + if (await stats(path.join(layout.agentSkillsDir, name))) { + drift.push({ kind: "extra", path: `${name} (retired)` }) + } + } const linkDrift: string[] = [] if (layout.claudeMode === "root-link") { @@ -255,6 +282,14 @@ export async function inspectDrift(layout: TargetLayout, canonicalSkills = skill linkDrift.push(`link .claude/skills/${name} -> ${value}`) } } + for (const name of layout.retiredSkills) { + const link = path.join(layout.claudeSkills, name) + const current = await stats(link) + if (current) { + const value = current.isSymbolicLink() ? await readlink(link) : "conflict" + linkDrift.push(`extra .claude/skills/${name} -> ${value}`) + } + } } return { @@ -353,7 +388,7 @@ export async function writeSkills( } await hooks.afterStage?.() - for (const name of layout.skills) { + for (const name of [...layout.skills, ...layout.retiredSkills]) { const destination = path.join(layout.agentSkillsDir, name) if (await stats(destination)) { await rename(destination, path.join(skillBackupRoot, name)) @@ -394,6 +429,9 @@ export async function writeSkills( ) createdLinks.push(link) } + for (const name of layout.retiredSkills) { + await backupLink(path.join(layout.claudeSkills, name), `retired-${name}`) + } } await removeEntry(transaction) diff --git a/skills/path/SKILL.md b/skills/learn/SKILL.md similarity index 73% rename from skills/path/SKILL.md rename to skills/learn/SKILL.md index e15d6ff..252b315 100644 --- a/skills/path/SKILL.md +++ b/skills/learn/SKILL.md @@ -1,9 +1,9 @@ --- -name: path -description: Start, plan, teach, and resume personal learning in a PathMX learning space. Use automatically when one learner asks to learn, study, practice, build a curriculum, continue a learning path, review progress, or turn a goal into guided lessons, including when a new personal learning repository must be created. +name: learn +description: Start, plan, guide, and resume personal learning in a PathMX learning space. Use automatically when one learner asks to learn, study, practice, build a personal curriculum, continue a learning path, review progress, or turn a goal into guided lessons, including when a new personal learning repository must be created. Use teach instead when authoring one path for multiple learners. --- -# Personal Learning Path +# Learn with PathMX Build a durable personal learning space for one learner. Use the installed `pathmx` skill for PathMX syntax, Player routes, and verification. @@ -73,7 +73,9 @@ Turn onboarding evidence into: - **later modules:** provisional titles and outcomes. Write the proposed map into the learning repository before showing it to the -learner. Use the bundled `assets/path/index.path.md` scaffold so every one of +learner. Use the shared +[`library/templates/learn/path/index.path.md`](../pathmx/library/templates/learn/path/index.path.md) +scaffold so every one of the 3–7 milestones has one visible `planned`, `ready`, `in progress`, `demonstrated`, or `paused` status plus an evidence target. Link the proposed foreground Path from the home Source. This first useful artifact should be @@ -99,6 +101,12 @@ module. A request to show a map is not confirmation. Do not fully author the entire future curriculum. Fully author the current module and keep later modules easy to change. +After the persisted map is visible, use background workers when the agent +surface supports them to gather low-risk subject research, prerequisite risks, +or candidate examples while the learner reviews the proposal. Do not draft +session Sources before confirmation. Keep the parent agent available for the +learner's reply, and stop or redirect work when the goal changes. + ## Build a learning runway Prepare all sessions in the current module before asking the learner to begin. @@ -110,17 +118,43 @@ Before calling the module ready, check every session for a worked example, an optional hint or smaller attempt, and an immediate rationale, self-check, or rubric. Keep focused review and an optional stretch task ready in the module. -For a standard two-session module, copy the bundled `assets/module/` scaffold -into the new module directory and replace its author notes. Add or remove -session files only when the learner's confirmed rhythm calls for it. +For a standard two-session module, copy the shared +[`library/templates/learn/module/`](../pathmx/library/templates/learn/module/) +scaffold into the new module directory and replace its author notes. Add or +remove session files only when the learner's confirmed rhythm calls for it. + +When subagents are available, let the parent agent write the confirmed module +contract, shared vocabulary or scenario, index, and session skeletons first. +Then delegate only bounded, independent work: + +- research or fact-check the subject, prerequisites, and examples; +- draft distinct later session, review, or checkpoint Sources with one owner + per file; +- review completed drafts for learning alignment, accessibility, and PathMX + correctness. + +The parent agent owns the learner conversation, the first session, shared +indexes, profile and activity state, integration, and final verification. Give +workers only the confirmed outcomes and minimum learning-relevant context; do +not expose unnecessary personal details. Prefer two or three direct workers, +do not ask them to delegate further, and never let concurrent workers edit the +same file. If delegation is unavailable or coordination would take longer than +the work, continue locally without blocking the learner. + +Give workers the same terminology, example or data model, prerequisites, and +link targets. Ask each authoring worker for one focused build or content check; +the parent runs the full repository check once after integration. Set the join +point before learner handoff. If a worker misses it, the parent completes or +reassigns that output instead of making the learner wait. Work in visible stages when the agent surface supports progress updates: -1. Create the module index and session skeletons, then report that the runway - exists. -2. Fill the first session, then report the concrete capability now ready. -3. Fill the remaining sessions, review, and checkpoint, then report that the - uninterrupted module is ready for verification. +1. Create the module index, contract, and session skeletons; start any bounded + workers; then report that the runway exists. +2. Fill the first session while independent later Sources are prepared, then + report the concrete capability now ready. +3. Integrate and review every session, review, and checkpoint, then report that + the uninterrupted module is ready for verification. 4. Run one targeted build or route check, then one full check before handoff. Keep these updates factual and brief; they are learner-visible progress, not @@ -173,6 +207,13 @@ At the module checkpoint: 4. Update current Point A and prepare the next module. 5. Record a short synthesis and the reason for placement. +After checkpoint evidence is durable, a read-only worker may summarize the +evidence against the existing rubric while another researches the likely next +module. The parent agent must make the placement decision, write Point A and +activity changes, prepare remediation or the next module, and explain the +decision to the learner. Never wait for optional worker output before +acknowledging the learner or answering a direct question. + Gate progression only when a later capability genuinely depends on a missed core idea. After repeated difficulty, provide a smaller remediation module or renegotiate the goal or pace. Do not trap the learner in a remediation loop. @@ -235,9 +276,11 @@ paths/ - A module index links its fully prepared sessions and checkpoint. - Completed Sources remain history. Do not silently rewrite past evidence. -Read the [buffered loop](./references/buffered-loop.md) and the compact -[worked example](./references/worked-example.md) when planning or changing a -path. +Read the [buffered loop](./references/buffered-loop.md) when planning or +changing a path. Use the shared PathMX [library](../pathmx/library/index.md) to +choose verified patterns, templates, components, and examples. The compact +[SQL example](../pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/index.path.md) +shows one complete personal module. ## Learning rules diff --git a/skills/learn/agents/openai.yaml b/skills/learn/agents/openai.yaml new file mode 100644 index 0000000..ce1980f --- /dev/null +++ b/skills/learn/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Learn with PathMX" + short_description: "Build buffered adaptive learning paths" + default_prompt: "Use $learn to help me start or continue learning in my personal PathMX space." diff --git a/skills/path/references/buffered-loop.md b/skills/learn/references/buffered-loop.md similarity index 61% rename from skills/path/references/buffered-loop.md rename to skills/learn/references/buffered-loop.md index 938f3af..21dd2ed 100644 --- a/skills/path/references/buffered-loop.md +++ b/skills/learn/references/buffered-loop.md @@ -33,6 +33,40 @@ flowchart TD Review --> Checkpoint ``` +## Fast orchestration lane + +Use subagents only when the active surface supports them and the work has +independent outputs. The parent agent stays in the learner conversation and +owns every durable state transition. Workers return concise drafts or findings +to the parent; they do not teach, place, or hand off directly to the learner. + +| Phase | Parent agent | Safe worker lane | +| --- | --- | --- | +| Map proposal | Gather evidence, write and show the persisted map, answer the learner | After the map is visible, research vocabulary, prerequisite risks, or examples that survive likely map edits | +| Await confirmation | Remain responsive and revise the proposal | Continue only low-risk research; do not draft session Sources | +| Confirmed runway | Write the module contract, shared index, skeletons, and first session; integrate and verify | Fact-check or draft distinct later session, review, or checkpoint files | +| Learner in module | Respond to questions and preserve ready material | Investigate a bounded factual question or annotation without rewriting the module speculatively | +| Checkpoint | Make placement, remediation, and Point A decisions; write shared state | Independently summarize durable evidence against the existing rubric or research the likely next module | + +Use the smallest useful team, normally the parent plus two or three direct +workers. Give every worker one output, one owner, and a clear return shape. +Prefer read-only research and reviews; when workers author, assign separate +files. Do not allow nested delegation, concurrent edits to shared indexes, +profile or activity files, or unreviewed worker output in learner-facing +Sources. + +Share confirmed outcomes, module dependencies, audience level, shared +terminology, example or data model, link targets, and the exact file boundary. +Ask workers for focused checks only; the parent runs one full check after +integration. Omit learner identity and any profile detail that the worker does +not need. + +Set a join point before learner handoff. If a worker misses it, conflicts, or +costs more to coordinate than the remaining task, the parent completes or +reassigns that output instead of waiting. If the learner changes the goal, the +parent redirects or stops stale work. If workers are unavailable, continue in +the parent without mentioning an internal tooling limitation. + ## One module A module has one coherent capability destination. Prepare all its sessions diff --git a/skills/manifest.json b/skills/manifest.json index 5fc4313..f13ebd8 100644 --- a/skills/manifest.json +++ b/skills/manifest.json @@ -10,10 +10,18 @@ "dependsOn": [] }, { - "name": "path", - "directory": "skills/path", + "name": "learn", + "directory": "skills/learn", "invocation": "automatic-and-explicit", "purpose": "Create and resume buffered adaptive personal learning paths.", + "dependsOn": ["pathmx"], + "replaces": ["path"] + }, + { + "name": "teach", + "directory": "skills/teach", + "invocation": "automatic-and-explicit", + "purpose": "Design, author, and review PathMX paths for multiple learners.", "dependsOn": ["pathmx"] } ] diff --git a/skills/path/agents/openai.yaml b/skills/path/agents/openai.yaml deleted file mode 100644 index 7af61ec..0000000 --- a/skills/path/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Personal Learning Path" - short_description: "Build buffered adaptive learning paths" - default_prompt: "Use $path to help me start or continue learning in my personal PathMX space." diff --git a/skills/path/references/worked-example.md b/skills/path/references/worked-example.md deleted file mode 100644 index 2f7dfc7..0000000 --- a/skills/path/references/worked-example.md +++ /dev/null @@ -1,129 +0,0 @@ -# Worked Example - -This compact SQL example shows the durable shape of one buffered module. It is -not a required topic or exact file count. - -## Path map - -`paths/sql-foundations/index.path.md`: - -```md ---- -type: path -status: active ---- - -# SQL Foundations - -**Point A:** Can filter one table. Joins are unfamiliar. - -**Point B:** Can build and explain a small support report across related tables. - -## Milestones - -- **Choose rows from one table:** demonstrated -- **Combine related tables:** in progress -- **Summarize joined results:** planned -- **Build the support report:** planned - -## Current module - -[Combine related tables](./modules/01-combine-tables/index.path.md) -``` - -## Current module - -`paths/sql-foundations/modules/01-combine-tables/index.path.md`: - -```md ---- -type: path -status: ready ---- - -# Combine Related Tables - -**Destination:** Choose `INNER JOIN` or `LEFT JOIN`, write the join condition, -and explain which unmatched rows remain. - -1. [See what a join preserves](./01-rows-that-survive.lesson.md) -2. [Choose the join from the question](./02-choose-the-join.lesson.md) -3. [Optional review](./review.practice.md) -4. [Milestone checkpoint](./milestone.assessment.md) -``` - -Both sessions exist before the learner starts the module. - -## One uninterrupted session - -`01-rows-that-survive.lesson.md`: - -````md ---- -type: lesson -status: ready ---- - -# See What a Join Preserves - -By the end, you will predict which ticket rows survive an `INNER JOIN` and a -`LEFT JOIN`. - ---- - - - -## Start with one worked example - -```sql -SELECT t.id, c.name -FROM tickets t -LEFT JOIN customers c ON c.id = t.customer_id; -``` - -A `LEFT JOIN` keeps every ticket. When a customer record is missing, the -customer columns are empty instead of the ticket disappearing. - ---- - - - -## Predict before revealing - -What changes if `LEFT JOIN` becomes `INNER JOIN`? - -Write one sentence before continuing. - ---- - - - -## Compare your prediction - -`INNER JOIN` keeps only tickets with a matching customer. The important -question is not “Which keyword do I remember?” but “Which rows must survive?” - ---- - - - -## Apply it - -Choose a join for a report that must include every ticket, then explain the -choice. Use the optional review if the row-survival rule is still fuzzy. - -[Continue to Session 2](./02-choose-the-join.lesson.md) -```` - -The learner can complete the session without another agent turn. They may -annotate any confusing explanation for later review. - -## Milestone checkpoint - -The checkpoint asks for one small support query and explanation. The agent -compares that artifact with the module's evidence target, records a synthesis -in `paths/learning.activity.md`, and either marks the milestone demonstrated or -prepares focused review. - -The next module starts from that recorded evidence, not from a generic course -sequence. diff --git a/skills/pathmx/SKILL.md b/skills/pathmx/SKILL.md index 50eb101..fdd699f 100644 --- a/skills/pathmx/SKILL.md +++ b/skills/pathmx/SKILL.md @@ -1,6 +1,6 @@ --- name: pathmx -description: Author, revise, review, play, and verify PathMX sources. Use automatically for PathMX Markdown, Sources, Blocks, Beats, links, directives, questions, annotations, Literate Components, Play pacing, Player routes, media, code, math, styling, configuration, CLI setup, builds, or browser review in any PathMX repository. +description: Author, revise, review, play, and verify PathMX sources. Use automatically for PathMX Markdown, Sources, Blocks, Beats, links, directives, questions, annotations, Literate Components, Play pacing, Player routes, icons, media, code, math, styling, configuration, CLI setup, builds, or browser review in any PathMX repository. --- # PathMX @@ -75,6 +75,7 @@ Read only what the task needs: - [Questions and responses](./references/pathmx-questions.md) - [Annotations](./references/pathmx-annotations.md) - [Literate Components](./references/pathmx-literate-components.md) +- [Lucide icons](./references/pathmx-icons.md) - [Code](./references/pathmx-code.md) - [Math](./references/pathmx-math.md) - [Media](./references/pathmx-media.md) @@ -82,6 +83,7 @@ Read only what the task needs: - [Configuration](./references/pathmx-config.md) - [Tooling, setup, and verification](./references/pathmx-tooling.md) - [Small repository example](./references/pathmx-repo-example/pathmx-repository.md) +- [Shared pattern, template, component, and example library](./library/index.md) ## Boundaries diff --git a/skills/pathmx/library/components/feedback-panel.components.md b/skills/pathmx/library/components/feedback-panel.components.md new file mode 100644 index 0000000..2a12ef8 --- /dev/null +++ b/skills/pathmx/library/components/feedback-panel.components.md @@ -0,0 +1,17 @@ +--- +componentName: feedback-panel +--- + +# Feedback Panel + +```html + +``` + +```css +:self { + border: 1px solid currentColor; + border-radius: 0.75rem; + padding: 1rem; +} +``` diff --git a/tests/fixtures/path/paths/learner.profile.md b/skills/pathmx/library/examples/learn-sql-foundations/paths/learner.profile.md similarity index 100% rename from tests/fixtures/path/paths/learner.profile.md rename to skills/pathmx/library/examples/learn-sql-foundations/paths/learner.profile.md diff --git a/tests/fixtures/path/paths/learning.activity.md b/skills/pathmx/library/examples/learn-sql-foundations/paths/learning.activity.md similarity index 100% rename from tests/fixtures/path/paths/learning.activity.md rename to skills/pathmx/library/examples/learn-sql-foundations/paths/learning.activity.md diff --git a/tests/fixtures/path/paths/sql-foundations/index.path.md b/skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/index.path.md similarity index 100% rename from tests/fixtures/path/paths/sql-foundations/index.path.md rename to skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/index.path.md diff --git a/tests/fixtures/path/paths/sql-foundations/modules/01-combine-tables/01-rows-that-survive.lesson.md b/skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/modules/01-combine-tables/01-rows-that-survive.lesson.md similarity index 100% rename from tests/fixtures/path/paths/sql-foundations/modules/01-combine-tables/01-rows-that-survive.lesson.md rename to skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/modules/01-combine-tables/01-rows-that-survive.lesson.md diff --git a/tests/fixtures/path/paths/sql-foundations/modules/01-combine-tables/02-choose-the-join.lesson.md b/skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/modules/01-combine-tables/02-choose-the-join.lesson.md similarity index 100% rename from tests/fixtures/path/paths/sql-foundations/modules/01-combine-tables/02-choose-the-join.lesson.md rename to skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/modules/01-combine-tables/02-choose-the-join.lesson.md diff --git a/tests/fixtures/path/paths/sql-foundations/modules/01-combine-tables/index.path.md b/skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/modules/01-combine-tables/index.path.md similarity index 100% rename from tests/fixtures/path/paths/sql-foundations/modules/01-combine-tables/index.path.md rename to skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/modules/01-combine-tables/index.path.md diff --git a/tests/fixtures/path/paths/sql-foundations/modules/01-combine-tables/milestone.assessment.md b/skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/modules/01-combine-tables/milestone.assessment.md similarity index 100% rename from tests/fixtures/path/paths/sql-foundations/modules/01-combine-tables/milestone.assessment.md rename to skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/modules/01-combine-tables/milestone.assessment.md diff --git a/tests/fixtures/path/paths/sql-foundations/modules/01-combine-tables/review.practice.md b/skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/modules/01-combine-tables/review.practice.md similarity index 100% rename from tests/fixtures/path/paths/sql-foundations/modules/01-combine-tables/review.practice.md rename to skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/modules/01-combine-tables/review.practice.md diff --git a/tests/fixtures/path/paths/sql-foundations/path.outcome.md b/skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/path.outcome.md similarity index 100% rename from tests/fixtures/path/paths/sql-foundations/path.outcome.md rename to skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/path.outcome.md diff --git a/skills/pathmx/library/examples/shared-components/index.path.md b/skills/pathmx/library/examples/shared-components/index.path.md new file mode 100644 index 0000000..9474705 --- /dev/null +++ b/skills/pathmx/library/examples/shared-components/index.path.md @@ -0,0 +1,17 @@ +--- +type: path +status: ready +--- + +# Shared Component Example + +This fictional example shows one small component from the library. + + + +Compare the result with the stated outcome. Name one choice you would keep and +one you would revise. + + + +[@components]: ../../components/feedback-panel.components.md diff --git a/skills/pathmx/library/index.md b/skills/pathmx/library/index.md new file mode 100644 index 0000000..4af0461 --- /dev/null +++ b/skills/pathmx/library/index.md @@ -0,0 +1,33 @@ +# PathMX Library + +Use this library for reusable learner-facing material. Every item is local, +fictional where it depicts learners, and verified against the repository's +pinned PathMX baseline. + +## Choose an item + +| Kind | Use | Contents | +| --- | --- | --- | +| Pattern | Apply a stable design shape without copying syntax | [Complete session arc](./patterns/complete-session-arc.md) | +| Template | Copy and replace author notes | [Personal learning](./templates/learn/path/index.path.md), [shared path](./templates/teach/path/index.path.md) | +| Component | Copy a verified Literate Component | [Feedback panel](./components/feedback-panel.components.md) | +| Example | Inspect a complete fictional implementation | [Personal SQL module](./examples/learn-sql-foundations/paths/sql-foundations/index.path.md), [shared component use](./examples/shared-components/index.path.md) | + +## Use the library + +1. Read the destination repository instructions and pinned PathMX version. +2. Choose the smallest relevant item. Do not copy an entire example when one + pattern or component is enough. +3. Copy templates and components into the destination repository. Never make + learner Sources depend on an installed skill directory at runtime. +4. Replace every author note, fictional detail, placeholder, and irrelevant + convention. +5. Preserve readable Markdown and relative links. +6. Build, resolve the exact route, and review the result in Player. + +Library status means the PathMX contract is stable at the pinned baseline. It +does not mean the learning design fits every audience. Adapt through `learn` +for one learner or `teach` for a path shared by many learners. + +Keep experimental or diagnostic-only syntax in test fixtures until it is +useful, documented, and ready for reuse. diff --git a/skills/pathmx/library/patterns/complete-session-arc.md b/skills/pathmx/library/patterns/complete-session-arc.md new file mode 100644 index 0000000..bc5aa6d --- /dev/null +++ b/skills/pathmx/library/patterns/complete-session-arc.md @@ -0,0 +1,18 @@ +# Complete Session Arc + +Use this pattern for a session a learner can finish without waiting for an +author or agent. + +1. **Orient:** name the value, destination, and session map. +2. **Model:** work one concrete example and explain the decisions. +3. **Guide:** offer a similar attempt with a staged hint or smaller version. +4. **Apply:** ask for independent, varied, or transfer practice. +5. **Check:** provide an answer rationale, comparison, self-check, or rubric. +6. **Reflect:** invite a short note or annotation when it will support review. +7. **Complete:** state the capability practiced and link the next useful step. + +Use Blocks for meaningful phases and Beats for useful reveals. Keep the Source +readable outside Play mode. Place help beside the attempt it supports. + +For `learn`, tune the arc to confirmed evidence and preferences. For `teach`, +make entry assumptions and recovery routes work for the intended audience. diff --git a/skills/path/assets/module/01-session.lesson.md b/skills/pathmx/library/templates/learn/module/01-session.lesson.md similarity index 100% rename from skills/path/assets/module/01-session.lesson.md rename to skills/pathmx/library/templates/learn/module/01-session.lesson.md diff --git a/skills/path/assets/module/02-session.lesson.md b/skills/pathmx/library/templates/learn/module/02-session.lesson.md similarity index 100% rename from skills/path/assets/module/02-session.lesson.md rename to skills/pathmx/library/templates/learn/module/02-session.lesson.md diff --git a/skills/path/assets/module/index.path.md b/skills/pathmx/library/templates/learn/module/index.path.md similarity index 100% rename from skills/path/assets/module/index.path.md rename to skills/pathmx/library/templates/learn/module/index.path.md diff --git a/skills/path/assets/module/milestone.assessment.md b/skills/pathmx/library/templates/learn/module/milestone.assessment.md similarity index 100% rename from skills/path/assets/module/milestone.assessment.md rename to skills/pathmx/library/templates/learn/module/milestone.assessment.md diff --git a/skills/path/assets/module/review.practice.md b/skills/pathmx/library/templates/learn/module/review.practice.md similarity index 100% rename from skills/path/assets/module/review.practice.md rename to skills/pathmx/library/templates/learn/module/review.practice.md diff --git a/skills/path/assets/path/index.path.md b/skills/pathmx/library/templates/learn/path/index.path.md similarity index 100% rename from skills/path/assets/path/index.path.md rename to skills/pathmx/library/templates/learn/path/index.path.md diff --git a/skills/pathmx/library/templates/teach/module/01-session.lesson.md b/skills/pathmx/library/templates/teach/module/01-session.lesson.md new file mode 100644 index 0000000..c40563d --- /dev/null +++ b/skills/pathmx/library/templates/teach/module/01-session.lesson.md @@ -0,0 +1,40 @@ +--- +type: lesson +status: draft +--- + +# Session 1 + + + +## Destination + + + +--- + +## Worked example + + + +--- + +## Guided attempt + + + +--- + +## Apply it + + + +## Check your work + + + +--- + +## Finish + + diff --git a/skills/pathmx/library/templates/teach/module/02-session.lesson.md b/skills/pathmx/library/templates/teach/module/02-session.lesson.md new file mode 100644 index 0000000..fae848f --- /dev/null +++ b/skills/pathmx/library/templates/teach/module/02-session.lesson.md @@ -0,0 +1,40 @@ +--- +type: lesson +status: draft +--- + +# Session 2 + + + +## Destination + + + +--- + +## Varied example + + + +--- + +## Supported practice + + + +--- + +## Transfer + + + +## Check your work + + + +--- + +## Finish + + diff --git a/skills/pathmx/library/templates/teach/module/index.path.md b/skills/pathmx/library/templates/teach/module/index.path.md new file mode 100644 index 0000000..74cb976 --- /dev/null +++ b/skills/pathmx/library/templates/teach/module/index.path.md @@ -0,0 +1,21 @@ +--- +type: path +status: draft +--- + +# Shared module + + + +## Before you begin + + + +## Sessions + +1. [Session 1](./01-session.lesson.md) +2. [Session 2](./02-session.lesson.md) +3. [Module evidence check](./module.assessment.md) + +Each session includes embedded support and a next step. Learners should not +need the author present to repair ordinary instructions. diff --git a/skills/pathmx/library/templates/teach/module/module.assessment.md b/skills/pathmx/library/templates/teach/module/module.assessment.md new file mode 100644 index 0000000..974748a --- /dev/null +++ b/skills/pathmx/library/templates/teach/module/module.assessment.md @@ -0,0 +1,19 @@ +--- +type: assessment +status: draft +--- + +# Module evidence check + +## What to do + + + +## Success criteria + + + +## After the check + + diff --git a/skills/pathmx/library/templates/teach/path/index.path.md b/skills/pathmx/library/templates/teach/path/index.path.md new file mode 100644 index 0000000..ad3640a --- /dev/null +++ b/skills/pathmx/library/templates/teach/path/index.path.md @@ -0,0 +1,41 @@ +--- +type: path +status: draft +--- + +# Shared learning path + + + +## Who this is for + + + +## What you will be able to do + + + +## What you need + + + +## Journey + +1. **First capability:** mapped + - Evidence: +2. **Second capability:** mapped + - Evidence: +3. **Final transfer:** mapped + - Evidence: + + + +## How to take this path + + + +## Ready scope + + diff --git a/skills/pathmx/references/pathmx-icons.md b/skills/pathmx/references/pathmx-icons.md new file mode 100644 index 0000000..c9f3fd9 --- /dev/null +++ b/skills/pathmx/references/pathmx-icons.md @@ -0,0 +1,60 @@ +# Lucide Icons + +Use PathMX Lucide shortcodes for small inline cues, link labels, and +UI-adjacent content. Keep meaningful text visible whenever space allows. + +## Syntax + +```md +:lucide-sparkles: +:lucide-info[Information]: +[:lucide-book-open: Read the lesson](./lesson.md) +[:lucide-arrow-right[Continue]:](./next.md) +``` + +Use a Lucide icon name in kebab-case after `lucide-`. Do not repeat the prefix +inside the name. + +Shortcodes work in prose, headings, lists, Markdown link labels, and text nodes +inside authored HTML: + +

:lucide-pen-tool: Author with ordinary Markdown.

+ +This decorative cue :lucide-sparkles: inherits the current text color. + +[:lucide-arrow-right[Continue to icon choices]:](#choose-icons) + +[:lucide-book-open: Review icon choices](#choose-icons) + +## Accessibility + +- Use `:lucide-name:` when the icon is decorative. PathMX hides it from + assistive technology. +- Use `:lucide-name[Label]:` when the icon alone carries meaning. PathMX emits + `role="img"` and the label as `aria-label`. +- Prefer a decorative icon plus visible link or button text. Use a labeled + icon-only control only when the surrounding interface makes the action clear. +- Do not use an icon as the only distinction between success, warning, and + error states. + +## Choose icons + +Use names from the Lucide set. Confirm uncertain names with a targeted build; +do not invent names from memory. An unknown name is a build error, and PathMX +keeps the original shortcode as readable fallback text. + +## Styling + +Icons render at `1em` with `currentColor`. Set `font-size` and `color` on the +surrounding element when possible. Use `.pmx-icon` for the wrapper and +`.pmx-icon__svg` for the SVG only when a local treatment needs a direct hook. + +## Boundaries + +- Keep shortcode examples in inline or fenced code when they should remain + literal. PathMX also ignores shortcodes in comments, HTML attributes, and + `code` or `pre` elements. +- Use Lucide only. PathMX does not provide a generic icon-pack shortcode. +- Do not add remote icon loading, icon fonts, size attributes, stroke-width + attributes, variants, or custom shortcode classes; those contracts are not + supported by this syntax. diff --git a/skills/pathmx/references/pathmx-styling.md b/skills/pathmx/references/pathmx-styling.md index 4941dfa..fa29d24 100644 --- a/skills/pathmx/references/pathmx-styling.md +++ b/skills/pathmx/references/pathmx-styling.md @@ -12,10 +12,61 @@ Use the narrowest layer that owns the visual choice. | Need | Use | | --- | --- | | Color, type, measure, or shape tokens | `theme` frontmatter | +| Select a Source or Block root for reusable CSS | `styles.classes` in frontmatter or Block topmatter | | CSS for one Block | `[@styles]` in that Block | | CSS for one root graph | `[@root.styles]` in the root Source | | CSS inside a component | Component CSS with `:self` | +## Select Source and Block roots + +Add project-owned CSS class tokens under `styles.classes`: + +```md +--- +styles: + classes: [landing-page] +--- + +# Welcome + +--- + + + +## Start here +``` + +The Source renders as `.pmx-document.landing-page`. The Block renders as +`.pmx-block.pmx-prose.page-header.full-bleed`. Core classes remain first; +duplicate authored tokens are removed while keeping authored order. + +`classes` must be an array with one non-empty CSS class token per entry. An +entry cannot contain whitespace, and the `pmx-` prefix is reserved for PathMX. +PathMX does not rename or interpret accepted tokens. Prefer semantic project +names for durable Sources. Utility tokens work when the repository's local CSS +pipeline supports them. + +Classes select roots; they do not load CSS or add behavior. Pair them with +`[@styles]`, `[@root.styles]`, or another project stylesheet: + +```css +.pmx-document.landing-page:has(> .pmx-block.page-header:first-child) { + padding-block-start: 0; +} + +.pmx-block.page-header > h1:first-child { + margin-block-start: 0; +} +``` + +Classes attach only to real Source and Block roots. Included content does not +gain another `.pmx-block`; put the class on the host Block when an include +composition needs a styling hook. + ## CSS imports ```md @@ -27,8 +78,10 @@ Use the narrowest layer that owns the visual choice. Targets must be local CSS. `@styles` follows its Block. `@root.styles` applies to the active root graph and is ignored with a warning outside the root. -Directive scope does not rewrite CSS selectors. Scope rules when they must not -match other documents: +Directive scope does not rewrite CSS selectors. Prefer authored +`styles.classes` when a treatment should be reusable. Use +`data-pathmx-source` only when a rule deliberately belongs to one Source +identity: ```css @scope (.pmx-document[data-pathmx-source="paths/example.lesson"]) { @@ -38,6 +91,53 @@ match other documents: } ``` +PathMX keeps its prose and feature baselines at zero specificity. Later +authored rules such as `h1 { font-size: ... }` override the default heading +scale without `!important` or selector-weight tricks. Use `:scope` when its +structural meaning is useful, not merely to win the cascade. + +## Compose graph and Source styles + +Core propagates `@root.styles` to every built Source and emits named or local +Source styles after the root stylesheet. Use three ownership layers: + +1. Put readable graph defaults in one root stylesheet. +2. Let Source `theme` frontmatter own local color, type, measure, and shape. +3. Use named `@styles` imports for opt-in treatments and local CSS for + document or Block flourishes. + +Use root CSS for shared project variables and element rules: + +```css +@scope (.pmx-document) { + :scope { + --project-code-size: 0.95em; + } + + :where(code, th) { + font-family: ui-monospace, monospace; + font-size: var(--project-code-size); + } +} +``` + +Local styles load after root styles and can refine them: + +```md +[@styles.lab]: ./lab.css +[@styles]: ./document.css +``` + +Do not enumerate Source IDs to opt out of graph styles. Do not depend on +generated `data-pathmx-*` markup unless this reference documents that attribute +as an authoring contract. + +Source `theme` frontmatter is independent from root CSS. Do not use a root +stylesheet to redeclare `--pmx-*` theme tokens when linked Sources also own +theme frontmatter unless the project's pinned PathMX fixture verifies the +intended cascade. Prefer Source theme frontmatter for PathMX theme tokens and +root CSS for shared structural rules. + ## Theme tokens ```yaml @@ -76,6 +176,10 @@ variables. Keep document structure, navigation, focus states, and component behavior stable. Prefer one restrained accent and clear surfaces over many decorative colors. +Use a readable body and heading face by default. Reserve full-mono treatments +for Sources that opt into a technical or lab style. Keep code, data, and short +technical labels monospaced without shrinking them below comfortable prose. + Do not record inferred accessibility needs or personal traits. Save only what the learner chose. @@ -112,4 +216,6 @@ theme: - Check keyboard focus, forced colors, and reduced motion. - Check print when print styles changed. - Check selector leakage and missing assets. +- Check an unthemed linked Source, a Source with `theme` frontmatter, and any + opt-in stylesheet treatment when root styles changed. - Build and review warnings. diff --git a/skills/teach/SKILL.md b/skills/teach/SKILL.md new file mode 100644 index 0000000..dbcf2c2 --- /dev/null +++ b/skills/teach/SKILL.md @@ -0,0 +1,140 @@ +--- +name: teach +description: Design, author, revise, pilot, and review PathMX learning paths intended to be shared with multiple learners. Use automatically when an educator, subject-matter expert, team, or organization asks to create a course, workshop, curriculum, onboarding path, public guide, cohort experience, or reusable learning journey. Use learn instead for one learner's adaptive personal space. +--- + +# Teach with PathMX + +Create a reusable learning path for a defined audience. Use the installed +`pathmx` skill for PathMX syntax, Player routes, components, and verification. + +Treat the path as a product learners can take without the author present. Make +the promise, entry assumptions, journey, support, and evidence legible in the +Sources themselves. + +## Frame the shared path + +Establish the smallest useful brief: + +1. Name the audience and the situation that brings them here. +2. State what learners should be able to do afterward. +3. Record required prior knowledge, tools, time, and access needs. +4. Name one observable final performance or artifact. +5. Decide whether the path is self-paced, facilitated, cohort-based, or a + blend. +6. Identify the distribution or deployment context already used by the + repository. Do not invent hosting behavior. + +Ask for examples of existing paths when their audience, pacing, voice, +structure, components, or deployment should shape the work. Inspect those +examples before committing to a new house style. + +Keep real learner identities, responses, and private cohort data out of the +authored path. Use fictional content in examples and tests. + +## Design backward from evidence + +Write a short path contract before detailed sessions: + +- audience and entry assumptions; +- learner-facing promise; +- 3–7 capability outcomes in a coherent sequence; +- evidence for each outcome; +- final performance or transfer task; +- expected duration and pacing; +- support, accessibility, and facilitation boundaries. + +Separate what every learner needs from optional depth. Do not make a personal +learner profile or append-only personal activity log part of a shared path. +Individual repositories may add those through `learn` after taking or adapting +the shared material. + +Read the [shared path contract](./references/shared-path-contract.md) before +planning a new path or reviewing one for release. + +## Map the whole learner journey + +Make the complete path visible before filling every lesson. Give each module: + +- one capability destination; +- an entry condition when sequencing matters; +- complete sessions that can be taken without an author turn; +- embedded help and immediate feedback; +- an observable check tied to the destination; +- a clear next step. + +Use the shared PathMX [library](../pathmx/library/index.md). Start from the +[shared-path template](../pathmx/library/templates/teach/path/index.path.md) +and the [shared-module template](../pathmx/library/templates/teach/module/) +when they fit. Reuse a library component only when it improves comprehension +or feedback. + +Author in useful releases. Fully prepare the first module and enough adjacent +material to test navigation and progression. Keep later module maps concrete +but easy to change until examples, subject review, or a pilot justify deeper +production. Do not label a partial path complete. + +## Author for independent use + +Give every session a complete arc: + +1. Orient learners to the value, destination, and session map. +2. Model the capability with a concrete worked example. +3. Guide a supported attempt with optional hints or a smaller version. +4. Ask for independent, varied, or transfer practice. +5. Provide an answer rationale, comparison, self-check, or rubric. +6. Offer a brief reflection or annotation prompt when it will inform revision. +7. Close with the practiced capability and an exact next step. + +Write in the audience's language. Explain prerequisites before depending on +them. Keep ordinary Markdown readable outside Play mode. Use Blocks and Beats +for learning rhythm, not decoration. + +Distinguish learner-facing Sources from author or facilitator notes. Never +leak answer keys through visible navigation or assume a facilitator will repair +missing instructions live. + +## Build support into the path + +Provide help at the point of need: + +- a staged hint; +- a smaller attempt; +- another example; +- a rationale or rubric after an attempt; +- an optional challenge; +- recovery and re-entry guidance. + +Use annotations as product feedback when the deployment supports them. Review +patterns of confusion, accessibility friction, and dead ends; revise the path +without overwriting learner-owned responses or comments. + +## Review before sharing + +Review the path in four passes: + +1. **Learning:** outcomes, practice, feedback, and checks align. +2. **Journey:** entry, navigation, pacing, recovery, and completion are clear. +3. **Access:** narrow screens, keyboard use, contrast, reduced motion, media + alternatives, and required tools are handled. +4. **PathMX:** links, routes, questions, components, build output, and Player + behavior pass against the repository's pinned version. + +Use fictional learner runs for smoke testing. Pilot with representative +learners when available, distinguish observed evidence from author opinion, +and record unresolved release risks. + +Resolve the exact starting Source with `bunx pathmx route`. At handoff, give +the verified start URL or repository-relative entry, duration, prerequisites, +ready scope, and any facilitator or deployment assumptions. + +## Boundaries + +- Use `learn` for one learner's durable adaptive space. +- Use `pathmx` for syntax and tooling details. +- Preserve the destination repository's conventions and pinned PathMX version. +- Copy library assets into the destination; do not make learner Sources depend + on an installed skill directory at runtime. +- Treat external deployment behavior as version-sensitive and verify it from + the provided repository or deployment example. +- Do not claim broad effectiveness from one pilot or one completion metric. diff --git a/skills/teach/agents/openai.yaml b/skills/teach/agents/openai.yaml new file mode 100644 index 0000000..bf7e7b6 --- /dev/null +++ b/skills/teach/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Teach with PathMX" + short_description: "Create PathMX paths for many learners" + default_prompt: "Use $teach to create a shareable PathMX learning path for my audience." diff --git a/skills/teach/references/shared-path-contract.md b/skills/teach/references/shared-path-contract.md new file mode 100644 index 0000000..2c3645e --- /dev/null +++ b/skills/teach/references/shared-path-contract.md @@ -0,0 +1,62 @@ +# Shared Path Contract + +Use this contract to plan a path that many learners can take and to review it +before sharing. + +## Brief + +| Decision | Record | +| --- | --- | +| Audience | Situation, relevant experience, and exclusions | +| Promise | What learners can do afterward | +| Entry | Prior knowledge, tools, access, and setup | +| Mode | Self-paced, facilitated, cohort, or blended | +| Pace | Expected sessions, duration, and stopping points | +| Final evidence | Observable performance, artifact, or explanation | +| Distribution | Verified repository or deployment context | + +Do not collapse a broad audience into a fictional “average learner.” State the +entry assumptions and provide a recovery route when a prerequisite is common +but not universal. + +## Alignment map + +For each capability outcome, name: + +1. the evidence that would demonstrate it; +2. the examples and explanation needed; +3. supported and independent practice; +4. immediate feedback learners can use; +5. the checkpoint or transfer task; +6. the next capability that depends on it. + +Remove activities that do not support an outcome. Add evidence or practice +when an outcome exists only as explanatory prose. + +## Release slices + +Use three honest states: + +- **Mapped:** audience, promise, full journey, and evidence are visible. +- **Pilot-ready:** the first useful slice is complete, navigable, supported, + and verified for representative learner use. +- **Release-ready:** the intended scope is complete, subject-reviewed, tested, + and accompanied by verified start and distribution instructions. + +Name partial scope precisely. A complete first module is not a complete path. + +## Review evidence + +Collect observations about: + +- where learners start or leave; +- time on meaningful tasks rather than page count alone; +- repeated wrong turns or unclear instructions; +- support used and support still missing; +- assessment evidence against the stated outcome; +- accessibility and device friction; +- annotations and facilitator interventions. + +Distinguish observed behavior, learner report, and author inference. Revise +shared material from patterns, while allowing individual learners to adapt it +through `learn`. diff --git a/tests/fixtures/pathmx/icons/unknown.path.md b/tests/fixtures/pathmx/icons/unknown.path.md new file mode 100644 index 0000000..9686838 --- /dev/null +++ b/tests/fixtures/pathmx/icons/unknown.path.md @@ -0,0 +1,7 @@ +--- +type: path +--- + +# Unknown Icon + +This icon name is intentionally invalid: :lucide-definitely-not-real: diff --git a/tests/fixtures/pathmx/styling/base.css b/tests/fixtures/pathmx/styling/base.css new file mode 100644 index 0000000..dcc19d0 --- /dev/null +++ b/tests/fixtures/pathmx/styling/base.css @@ -0,0 +1,9 @@ +@scope (.pmx-document) { + :scope { + --fixture-base-theme: active; + } + + :where(code, th) { + font-family: ui-monospace, monospace; + } +} diff --git a/tests/fixtures/pathmx/styling/index.path.md b/tests/fixtures/pathmx/styling/index.path.md new file mode 100644 index 0000000..ea751ee --- /dev/null +++ b/tests/fixtures/pathmx/styling/index.path.md @@ -0,0 +1,10 @@ +--- +type: path +--- + +# Styling Fixture + +- [Unthemed Source](./unthemed.path.md) +- [Themed Source](./themed.path.md) + +[@root.styles]: ./base.css diff --git a/tests/fixtures/pathmx/styling/lab.css b/tests/fixtures/pathmx/styling/lab.css new file mode 100644 index 0000000..35bb65b --- /dev/null +++ b/tests/fixtures/pathmx/styling/lab.css @@ -0,0 +1,11 @@ +@scope (.pmx-document.technical-lab) { + :scope { + --fixture-lab-theme: active; + font-family: ui-monospace, monospace; + } + + .pmx-block.callout { + border-inline-start: 0.25em solid var(--pmx-color-accent); + padding-inline-start: 1em; + } +} diff --git a/tests/fixtures/pathmx/styling/themed.path.md b/tests/fixtures/pathmx/styling/themed.path.md new file mode 100644 index 0000000..65d4fe9 --- /dev/null +++ b/tests/fixtures/pathmx/styling/themed.path.md @@ -0,0 +1,27 @@ +--- +type: path +theme: + color: + accent: "#c2410c" +styles: + classes: [technical-lab] +--- + +# Themed Source + +This Source owns its accent and opts into the full-mono lab treatment. + +--- + + + +## Reusable note + +This Block selects a reusable project treatment without depending on its +position or Source id. + +[@styles.lab]: ./lab.css diff --git a/tests/fixtures/pathmx/styling/unthemed.path.md b/tests/fixtures/pathmx/styling/unthemed.path.md new file mode 100644 index 0000000..e25f40c --- /dev/null +++ b/tests/fixtures/pathmx/styling/unthemed.path.md @@ -0,0 +1,9 @@ +--- +type: path +--- + +# Unthemed Source + +This Source inherits the graph stylesheet defaults. + +`const inherited = true` diff --git a/work-log/2026-07-20-adaptive-learning-loop.brief.md b/work-log/2026-07-20-adaptive-learning-loop.brief.md index 233dee9..cc1f4dd 100644 --- a/work-log/2026-07-20-adaptive-learning-loop.brief.md +++ b/work-log/2026-07-20-adaptive-learning-loop.brief.md @@ -3,11 +3,11 @@ status: superseded date: 2026-07-20 related: - https://build-week.pathmx.net/work-log/2026-07-18-player-native-learning-reshape.brief - - ../skills/path/SKILL.md - - ../skills/path/references/buffered-loop.md - - ../tests/fixtures/path/paths/learner.profile.md - - ../tests/fixtures/path/paths/learning.activity.md - - ../tests/fixtures/path/paths/sql-foundations/index.path.md + - ../skills/learn/SKILL.md + - ../skills/learn/references/buffered-loop.md + - ../skills/pathmx/library/examples/learn-sql-foundations/paths/learner.profile.md + - ../skills/pathmx/library/examples/learn-sql-foundations/paths/learning.activity.md + - ../skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/index.path.md --- # Adaptive Point A → Point B Learning Loop @@ -17,15 +17,15 @@ Extends the confirm a plan, teach in small stages, review after each lesson, assess before the next, then update the roadmap from evidence. -Kept here as the first design brief behind `/path` and as a record of Tram Le +Kept here as the first design brief behind `/learn` and as a record of Tram Le and Mark Johnson's early testing. That testing exposed the waiting and weak progress structure in the one-Block-at-a-time loop described below. The current -contract is the [buffered loop](../skills/path/references/buffered-loop.md), with -a compact [SQL fixture](../tests/fixtures/path/paths/sql-foundations/index.path.md). +contract is the [buffered loop](../skills/learn/references/buffered-loop.md), with +a compact [SQL library example](../skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/index.path.md). | | | | --- | --- | -| Canonical skill | `/path` in this repository | +| Canonical skill | `/learn` in this repository | | Product target | `pathmx-learning-starter` | | Demo topic | chess opening principles | | Memory | durable Sources under `paths/`, not chat | @@ -282,7 +282,8 @@ paths/ ``` The retired Chess prototype remains recoverable from Git history. The current -playable fixture lives under `tests/fixtures/path/paths/sql-foundations/`. +playable example lives under +`skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/`. Workflow briefs live in `work-log/`, not in synced learner Sources. --- diff --git a/work-log/index.md b/work-log/index.md index 7403b57..6175a4a 100644 --- a/work-log/index.md +++ b/work-log/index.md @@ -1,12 +1,12 @@ # Work log -Design briefs that informed `/path`. These are reference notes, not playable +Design briefs that informed `/learn`. These are reference notes, not playable PathMX Sources and not part of the synced skill packages. The skill contract is -[`skills/path/SKILL.md`](../skills/path/SKILL.md). +[`skills/learn/SKILL.md`](../skills/learn/SKILL.md). ## Entries - [Adaptive Point A → Point B Learning Loop](./2026-07-20-adaptive-learning-loop.brief.md) — the superseded first design. Its testing led to the current - [buffered loop](../skills/path/references/buffered-loop.md) and compact - [SQL fixture](../tests/fixtures/path/paths/sql-foundations/index.path.md). + [buffered loop](../skills/learn/references/buffered-loop.md) and compact + [SQL library example](../skills/pathmx/library/examples/learn-sql-foundations/paths/sql-foundations/index.path.md).