From c6b319eb4a6d91966e95326da5e16b758eb417e3 Mon Sep 17 00:00:00 2001 From: willwearing Date: Sat, 25 Apr 2026 11:13:42 -0600 Subject: [PATCH] Harden academy scaffolding for agents Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 19 ++--- README.md | 7 +- apps/site/eslint.config.mjs | 26 +++++++ .../web/src/app/(marketing)/docs/cli/page.tsx | 32 +++++++++ apps/web/src/app/llms-full.txt/route.ts | 52 +++++++++----- backend/.env.example | 1 + backend/src/main.ts | 20 +++++- docs/adding-a-course.md | 6 +- docs/api-reference.md | 53 +++++++++++++- packages/cli/README.md | 10 +-- .../__tests__/offline-commands.test.ts | 3 + packages/cli/src/commands/create-academy.ts | 6 +- packages/mcp/README.md | 8 ++- packages/mcp/src/__tests__/index.test.ts | 20 ++++++ packages/mcp/src/index.ts | 4 +- packages/shared/src/__tests__/domain.test.ts | 14 +++- packages/shared/src/scaffold.ts | 71 ++++++++++++++++++- 17 files changed, 302 insertions(+), 50 deletions(-) create mode 100644 apps/site/eslint.config.mjs diff --git a/CLAUDE.md b/CLAUDE.md index d5a62fb..d7a9d66 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,7 @@ If MCP is already configured, you have these tools available — no CLI needed: | Tool | Auth? | Description | |------|:---:|-------------| -| `graspful_create_academy` | No | Generate academy manifest YAML | +| `graspful_create_academy` | No | Generate academy plan and manifest YAML | | `graspful_scaffold_course` | No | Generate course YAML skeleton | | `graspful_fill_concept` | No | Add KPs and problems to a concept | | `graspful_validate` | No | Validate YAML against schema | @@ -70,7 +70,7 @@ This creates an account, org, and API key through browser auth. To use MCP tools ### Step 3: Build a course -The workflow is: scaffold -> fill -> validate -> review -> import. +The workflow is: academy plan -> course graphs -> fill -> validate -> review -> import. **Before writing any YAML**, follow the detailed runbook in `docs/adding-a-course.md`. Key steps: 1. Gather source material (official docs, syllabi, PDFs — not marketing copy) @@ -83,22 +83,25 @@ The workflow is: scaffold -> fill -> validate -> review -> import. 8. Validate and review ```bash -# 1. Scaffold the academy shell +# 1. Scaffold the academy plan graspful create academy --topic "Your Topic" -o academy.yaml -# 2. Scaffold the first course knowledge graph +# 2. Edit the academy plan until source material, learner promise, +# landing-page proof, and course dependencies are specific. + +# 3. Scaffold each course knowledge graph graspful create course --topic "Your Topic" --hours 10 -o course.yaml -# 3. Fill each concept with knowledge points and problems +# 4. Fill each concept with knowledge points and problems graspful fill concept course.yaml -# 4. Validate after every edit +# 5. Validate after every edit graspful validate course.yaml -# 5. Review — must score 10/10 to publish +# 6. Review must score 10/10 to publish graspful review course.yaml -# 6. Import and publish +# 7. Import and publish graspful import academy.yaml --org --course-dir . ``` diff --git a/README.md b/README.md index e634dd0..e3f2305 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Graspful is an agent-first academy creation platform. Academies and courses are ## How It Works -1. **Scaffold academy** -- `graspful create academy --topic "CKA Exam"` generates the academy shell +1. **Plan academy** -- `graspful create academy --topic "CKA Exam"` generates the academy layers, course map, and authoring gates 2. **Scaffold courses** -- `graspful create course --topic "Cluster Networking"` generates each course graph 3. **Fill** -- `graspful fill concept course.yaml networking` adds KPs and practice problems 4. **Review** -- `graspful review course.yaml` runs 10 quality checks, including whether problems only assess taught material @@ -24,6 +24,7 @@ npx @graspful/cli init # opens browser auth, then saves an API key locally graspful register --email you@example.com graspful create academy --topic "Your Topic" -o academy.yaml +mkdir -p courses graspful create course --topic "Foundations" -o courses/foundations.yaml ``` @@ -60,7 +61,7 @@ graspful/ |---------|:---:|-------------| | `graspful register` | No | Create account + API key via browser auth | | `graspful login` | No | Authenticate with existing credentials | -| `graspful create academy` | No | Generate academy manifest skeleton | +| `graspful create academy` | No | Generate academy plan and manifest skeleton | | `graspful create course` | No | Generate course YAML skeleton | | `graspful create brand` | No | Generate brand YAML with theme presets | | `graspful fill concept` | No | Add KPs and problems to a concept | @@ -99,7 +100,7 @@ Or manually add to your MCP config: | Tool | Auth? | Description | |------|:---:|-------------| | `graspful_scaffold_course` | No | Generate course YAML skeleton | -| `graspful_create_academy` | No | Generate academy manifest scaffold | +| `graspful_create_academy` | No | Generate academy plan and manifest scaffold | | `graspful_fill_concept` | No | Add KPs and problems to a concept | | `graspful_validate` | No | Validate YAML against schema | | `graspful_review_course` | No | Run 10 quality checks, including teaching alignment | diff --git a/apps/site/eslint.config.mjs b/apps/site/eslint.config.mjs new file mode 100644 index 0000000..f1ff46b --- /dev/null +++ b/apps/site/eslint.config.mjs @@ -0,0 +1,26 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + globalIgnores([ + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + "e2e/**", + "src/**/__tests__/**", + ]), + { + rules: { + "react-hooks/immutability": "off", + "react-hooks/refs": "off", + "react-hooks/set-state-in-effect": "off", + "react/no-unescaped-entities": "off", + }, + }, +]); + +export default eslintConfig; diff --git a/apps/web/src/app/(marketing)/docs/cli/page.tsx b/apps/web/src/app/(marketing)/docs/cli/page.tsx index af3ddfc..c76134b 100644 --- a/apps/web/src/app/(marketing)/docs/cli/page.tsx +++ b/apps/web/src/app/(marketing)/docs/cli/page.tsx @@ -187,6 +187,38 @@ export default function CLIReferencePage() { }`} /> + \\ + [--course ] \\ + [--version ] \\ + [-o, --output ]`} + description="Generate an academy manifest scaffold with the academy planning layers, course file references, and authoring gates for source material, learner promise, landing-page proof, graph checks, and review before publishing." + options={[ + { flag: "--topic ", description: "Academy topic name (required)" }, + { flag: "--course ", description: "Course name to include in dependency order. Repeatable. Defaults to the four academy planning layers." }, + { flag: "--version ", description: "Academy version string (default: 2026.1)" }, + { flag: "-o, --output ", description: "Output file path (defaults to stdout)" }, + ]} + examples={[ + { + label: "Scaffold a default academy plan", + code: `graspful create academy \\ + --topic "AWS Solutions Architect" \\ + -o aws-academy.yaml`, + }, + { + label: "Scaffold with named courses", + code: `graspful create academy \\ + --topic "PostHog TAM" \\ + --course "Data Models" \\ + --course "Pipeline Reading and Solution Design" \\ + -o posthog-tam-academy.yaml`, + }, + ]} + /> + \`, \`--no-browser\` | +| \`graspful create academy\` | No | Scaffold an academy plan with source, landing-page, graph, and review gates | \`--topic \`, \`--course \`, \`--version \` | | \`graspful create course\` | No | Scaffold a new course YAML | \`--topic \`, \`--hours \`, \`--source \` | | \`graspful fill concept \` | No | Generate knowledge points and problems for a concept | \`--force\` overwrite existing | | \`graspful validate \` | No | Validate course YAML against schema | — | | \`graspful review \` | No | Run all 10 quality checks | \`--fix\` auto-fix issues | | \`graspful describe \` | No | Describe course structure | — | | \`graspful create brand\` | No | Scaffold a brand YAML | \`--niche \`, \`--name \`, \`--domain \`, \`--org \` | -| \`graspful import \` | **Yes** | Import course to platform | \`--org \`, \`--publish\` | +| \`graspful import \` | **Yes** | Import an academy manifest or course to platform | \`--org \`, \`--publish\`, \`--course-dir \` | | \`graspful publish \` | **Yes** | Publish an imported course | \`--org \` | | \`graspful import-brand \` | **Yes** | Import brand config to platform | \`--org \` | | \`graspful list courses\` | **Yes** | List courses in an org | \`--org \` | @@ -82,9 +85,15 @@ graspful import course.yaml --org my-org --publish ## MCP Tools -Graspful exposes 10 MCP tools for AI agents. Tools marked (AUTH REQUIRED) need +Graspful exposes 12 MCP tools for AI agents. Tools marked (AUTH REQUIRED) need an API key in \`GRASPFUL_API_KEY\`. +### graspful_create_academy +Generate an academy manifest with planning layers and authoring gates. +- \`topic\` (string, required) — Academy topic +- \`courseNames\` (string[], optional) — Ordered course names. Defaults to foundations, core structures, operational flows, and applied judgment +- \`version\` (string, optional) — Academy version string + ### graspful_scaffold_course Scaffold a new course YAML from a topic. - \`topic\` (string, required) — The subject to create a course for @@ -110,6 +119,13 @@ Import a course YAML to the Graspful platform. Set \`GRASPFUL_API_KEY\` first if - \`orgSlug\` (string, required) — Organization slug - \`publish\` (boolean, optional) — Publish immediately after import +### graspful_import_academy (AUTH REQUIRED) +Import an academy manifest and referenced course YAMLs. Set \`GRASPFUL_API_KEY\` first if not authenticated. +- \`manifestYaml\` (string, required) — Full academy manifest YAML +- \`courseYamls\` (object, required) — Map of course file paths to course YAML strings +- \`org\` (string, required) — Organization slug +- \`publish\` (boolean, optional) — Publish imported courses after import + ### graspful_publish_course (AUTH REQUIRED) Publish an already-imported course. Set \`GRASPFUL_API_KEY\` first if not authenticated. - \`courseId\` (string, required) — ID of the course to publish @@ -358,12 +374,14 @@ Each check returns pass/fail with details. Fix failures before importing. ## Typical Agent Workflow 1. **Register** — Run \`graspful register\` in a terminal to complete browser auth and get an API key. This is required before importing or publishing. Skip if you already have GRASPFUL_API_KEY set. -2. **Scaffold** — Use \`graspful_scaffold_course(topic: "Your Topic", hours: 10)\` to generate the course skeleton. -3. **Fill concepts** — For each concept, call \`graspful_fill_concept(yaml, conceptId)\` to generate knowledge points and problems. -4. **Review** — Call \`graspful_review_course(yaml)\` to run quality checks. Fix any failures. -5. **Import** — Call \`graspful_import_course(yaml, orgSlug, publish: true)\` to push to platform. -6. **Create brand** (optional) — Use \`graspful_create_brand(niche: "Your Niche")\` to generate a white-label site config. -7. **Import brand** (optional) — Use \`graspful_import_brand(yaml, orgSlug)\` to deploy the site. +2. **Plan the academy** — Use \`graspful_create_academy(topic: "Your Topic")\` to generate the academy manifest, planning layers, and authoring gates. +3. **Resolve the plan** — Fill in source material, learner promise, landing-page proof, and course dependencies before writing knowledge points. +4. **Scaffold course graphs** — Use \`graspful_scaffold_course(topic: "Your Course", estimatedHours: 10)\` for each course referenced by the academy. +5. **Fill concepts** — For each concept, call \`graspful_fill_concept(yaml, conceptId)\` to generate knowledge points and problems. +6. **Review** — Call \`graspful_review_course(yaml)\` to run quality checks. Fix any failures. +7. **Import academy** — Call \`graspful_import_academy(manifestYaml, courseYamls, org, publish: true)\` to push the connected product to platform. +8. **Create brand** — Use \`graspful_create_brand(niche: "Your Niche", topic: "Your Topic")\` to generate the landing page config. +9. **Import brand** — Use \`graspful_import_brand(yaml, orgSlug)\` to deploy the site. ### Tips for Agents diff --git a/backend/.env.example b/backend/.env.example index f11a705..071ec98 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -2,6 +2,7 @@ NODE_ENV=development PORT=3000 +ALLOWED_ORIGINS=http://localhost:3001,http://localhost:3002 # Database (Supabase PostgreSQL) # Transaction pooler for app queries diff --git a/backend/src/main.ts b/backend/src/main.ts index b576c44..608d44e 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -40,9 +40,25 @@ async function bootstrap() { transform: true, })); - // Static origins from env var (platform hosts, localhost, etc.) + const nodeEnv = config.get('NODE_ENV', 'development'); + const defaultLocalOrigins = + nodeEnv === 'production' + ? [] + : [ + 'http://localhost:3001', + 'http://127.0.0.1:3001', + 'http://localhost:3002', + 'http://127.0.0.1:3002', + ]; + + // Static origins from env var plus local development app hosts. const staticOrigins = new Set( - config.get('ALLOWED_ORIGINS')?.split(',').filter(Boolean) ?? [], + [ + ...defaultLocalOrigins, + ...(config.get('ALLOWED_ORIGINS')?.split(',') ?? []), + ] + .map((origin) => origin.trim()) + .filter(Boolean), ); // Dynamic CORS: static origins are checked first, then brand domains from DB (cached 5 min) diff --git a/docs/adding-a-course.md b/docs/adding-a-course.md index bf47671..135e3e0 100644 --- a/docs/adding-a-course.md +++ b/docs/adding-a-course.md @@ -1,8 +1,8 @@ -# Adding a New Course +# Adding a New Academy or Course -End-to-end guide for creating a course YAML and importing it into the platform. +End-to-end guide for planning an academy, creating course YAML, and importing it into the platform. -This document also serves as the agent runbook for new course creation. If an agent is asked to "add a course", "draft a course graph", or "author a course YAML", this is the document it should follow. +This document also serves as the agent runbook for academy and course creation. If an agent is asked to "create an academy", "add a course", "draft a course graph", or "author a course YAML", this is the document it should follow. ## Agent Workflow diff --git a/docs/api-reference.md b/docs/api-reference.md index a0a92a7..3bfcd3a 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -8,6 +8,7 @@ Version: 0.1.0 - [1. CLI Reference](#1-cli-reference) - [Global Options](#global-options) + - [graspful create academy](#graspful-create-academy) - [graspful create course](#graspful-create-course) - [graspful create brand](#graspful-create-brand) - [graspful fill concept](#graspful-fill-concept) @@ -19,6 +20,7 @@ Version: 0.1.0 - [graspful login](#graspful-login) - [graspful register](#graspful-register) - [2. MCP Tools Reference](#2-mcp-tools-reference) + - [graspful_create_academy](#graspful_create_academy) - [graspful_scaffold_course](#graspful_scaffold_course) - [graspful_fill_concept](#graspful_fill_concept) - [graspful_validate](#graspful_validate) @@ -55,6 +57,35 @@ When `--format json` is set, all commands emit structured JSON to stdout and str --- +### `graspful create academy` + +Generate an academy plan and manifest scaffold with planning layers and authoring gates. + +If no `--course` flags are passed, the scaffold starts with the four default planning layers: foundations, core structures, operational flows, and applied judgment. It also includes an `authoringPlan` block for source material, learner promise, landing-page proof, graph checks, and review before publishing. + +**Syntax:** + +``` +graspful create academy --topic [options] +``` + +**Parameters:** + +| Flag | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `--topic ` | string | Yes | — | Academy topic name. | +| `--course ` | string[] | No | four planning layers | Course name to include in dependency order. Repeatable. | +| `--version ` | string | No | `2026.1` | Academy version string. | +| `-o, --output ` | string | No | stdout | Output file path. If omitted, YAML is printed to stdout. | + +**Example:** + +```bash +graspful create academy --topic "PostHog TAM" --course "Data Models" --course "Pipeline Reading" -o academy.yaml +``` + +--- + ### `graspful create course` Generate a course YAML scaffold with placeholder sections and concepts. @@ -630,12 +661,30 @@ You're ready. Run: graspful import course.yaml --org alice-org ## 2. MCP Tools Reference -The Graspful MCP server exposes 10 tools over the Model Context Protocol (stdio transport). Server name: `graspful`, version `0.1.0`. +The Graspful MCP server exposes 12 tools over the Model Context Protocol (stdio transport). Server name: `graspful`, version `0.1.0`. All tools return `{ content: [{ type: "text", text: "..." }], isError?: boolean }`. The `text` field contains either raw YAML or a JSON string depending on the tool. --- +### `graspful_create_academy` + +Generate an academy plan and manifest scaffold with planning layers and authoring gates. + +If `courseNames` is omitted, the scaffold starts with foundations, core structures, operational flows, and applied judgment. It also returns an `authoringPlan` block for source material, learner promise, landing-page proof, graph checks, and review before publishing. + +**Parameters:** + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `topic` | string | Yes | — | Academy topic name (e.g., "PostHog TAM"). | +| `courseNames` | string[] | No | four planning layers | Ordered course names for the manifest. | +| `version` | string | No | `2026.1` | Academy version string. | + +**Returns:** Raw academy manifest YAML string (not JSON-wrapped). + +--- + ### `graspful_scaffold_course` Generate a course YAML skeleton with sections, concepts, and prerequisite edges. @@ -1233,6 +1282,8 @@ Top-level key: `academy`. File: `academy-manifest.schema.ts`. Used to define a multi-course academy with optional grouping into "parts." +`graspful create academy` also emits an `authoringPlan` block. That block is for agents and humans while drafting: source material, learner promise, landing-page proof, graph checks, and review gates. The import API treats it as authoring metadata and does not persist it. + #### `academy` (required) | Field | Type | Required | Description | diff --git a/packages/cli/README.md b/packages/cli/README.md index e925321..66d394b 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -11,7 +11,7 @@ CLI for creating adaptive learning academies and courses from YAML knowledge gra # 1. Create an account (opens browser auth, then saves an API key locally) npx @graspful/cli register -# 2. Scaffold the academy shell and first course +# 2. Scaffold the academy plan and first course npx @graspful/cli create academy --topic "CKA Exam" -o cka-academy.yaml npx @graspful/cli create course --topic "CKA Exam Foundations" -o cka.yaml @@ -63,7 +63,7 @@ brand.yaml ──┘ with billing, analytics, | Command | Description | |---------|-------------| -| `graspful create academy` | Generate an academy manifest scaffold | +| `graspful create academy` | Generate an academy plan and manifest scaffold | | `graspful create course` | Generate a course YAML skeleton with knowledge graph structure | | `graspful create brand` | Generate a brand YAML with theme presets | | `graspful fill concept` | Add knowledge points and practice problems to a concept | @@ -95,7 +95,9 @@ graspful create course \ ### `graspful create academy` -Generate an academy manifest scaffold. Every Graspful product should be modeled as an academy, even if it starts with a single course. +Generate an academy plan and manifest scaffold. Every Graspful product should be modeled as an academy, even if it starts with a single course. + +If you do not pass `--course`, the scaffold starts with the four default planning layers: foundations, core structures, operational flows, and applied judgment. It also includes authoring gates for the source material, learner promise, landing-page proof, graph checks, and review before publishing. ```bash graspful create academy \ @@ -228,7 +230,7 @@ graspful validate course.yaml --format json The typical agent loop: -1. **Define the academy** -- `graspful create academy --topic "X"` to establish the academy shell +1. **Define the academy** -- `graspful create academy --topic "X"` to establish the academy plan 2. **Decompose the topic** -- break it into foundations, structures, operations, and applied judgment 3. **Scaffold each course** -- `graspful create course` for every course in the academy 4. **Edit** -- modify YAML to add concepts, adjust prerequisites, and keep the graph layered diff --git a/packages/cli/src/commands/__tests__/offline-commands.test.ts b/packages/cli/src/commands/__tests__/offline-commands.test.ts index c3b8e04..8375eab 100644 --- a/packages/cli/src/commands/__tests__/offline-commands.test.ts +++ b/packages/cli/src/commands/__tests__/offline-commands.test.ts @@ -85,7 +85,10 @@ describe('offline CLI commands', () => { const parsed = yaml.load(fs.readFileSync(outFile, 'utf-8')) as any; expect(parsed.academy).toBeDefined(); expect(parsed.academy.id).toBe('posthog-tam'); + expect(parsed.authoringPlan.sourceOfTruth.requiredBeforeGraphWork).toBe(true); + expect(parsed.parts).toHaveLength(4); expect(parsed.courses).toHaveLength(2); + expect(parsed.courses[0].part).toBe('foundations'); expect(parsed.courses[0].file).toBe('courses/data-models.yaml'); }); }); diff --git a/packages/cli/src/commands/create-academy.ts b/packages/cli/src/commands/create-academy.ts index 1f12d16..e5b0a61 100644 --- a/packages/cli/src/commands/create-academy.ts +++ b/packages/cli/src/commands/create-academy.ts @@ -13,11 +13,11 @@ function collect(value: string, previous: string[]) { export function registerCreateAcademyCommand(createCmd: Command) { createCmd .command('academy') - .description('Generate an academy manifest scaffold') + .description('Generate an academy plan and manifest scaffold') .requiredOption('--topic ', 'Academy topic name') .option( '--course ', - 'Course name to include in the academy manifest (repeatable). Defaults to one foundations course.', + 'Course name to include in the academy manifest (repeatable). Defaults to the four academy planning layers.', collect, [] as string[], ) @@ -45,7 +45,7 @@ export function registerCreateAcademyCommand(createCmd: Command) { fs.writeFileSync(opts.output, yamlContent); output( { file: opts.output, topic: opts.topic, courses: obj.courses.length }, - `Academy scaffold written to ${opts.output}`, + `Academy plan scaffold written to ${opts.output}`, ); } else { console.log(yamlContent); diff --git a/packages/mcp/README.md b/packages/mcp/README.md index ff8da06..6b08c44 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -61,7 +61,7 @@ Scaffold, fill, validate, and review work offline — no account needed. You onl | Tool | Description | Auth Required | |------|-------------|:---:| -| `graspful_create_academy` | Generate an academy manifest scaffold | No | +| `graspful_create_academy` | Generate an academy plan and manifest scaffold | No | | `graspful_scaffold_course` | Generate a course YAML skeleton with sections, concepts, and prerequisite edges | No | | `graspful_fill_concept` | Add knowledge points and problem stubs to a specific concept | No | | `graspful_validate` | Validate any Graspful YAML against its Zod schema. Auto-detects file type | No | @@ -78,12 +78,14 @@ Scaffold, fill, validate, and review work offline — no account needed. You onl ### `graspful_create_academy` -Generate an academy manifest scaffold. Every Graspful product should be modeled as an academy, even if it starts with a single course. +Generate an academy plan and manifest scaffold. Every Graspful product should be modeled as an academy, even if it starts with a single course. + +If `courseNames` is omitted, the scaffold starts with the four default planning layers: foundations, core structures, operational flows, and applied judgment. It also returns authoring gates for the source material, learner promise, landing-page proof, graph checks, and review before publishing. | Parameter | Type | Required | Description | |-----------|------|:---:|-------------| | `topic` | string | Yes | Academy topic (e.g., "PostHog TAM") | -| `courseNames` | string[] | No | Ordered course names for the manifest | +| `courseNames` | string[] | No | Ordered course names for the manifest. Defaults to the four academy planning layers. | | `version` | string | No | Academy version string | ### `graspful_scaffold_course` diff --git a/packages/mcp/src/__tests__/index.test.ts b/packages/mcp/src/__tests__/index.test.ts index fdb181a..0cd5726 100644 --- a/packages/mcp/src/__tests__/index.test.ts +++ b/packages/mcp/src/__tests__/index.test.ts @@ -38,6 +38,26 @@ describe('graspful_scaffold_course', () => { }); }); +describe('graspful_create_academy', () => { + test('returns an academy-first scaffold with authoring gates', async () => { + const result = await handleToolCall('graspful_create_academy', { + topic: 'PostHog TAM', + }); + const parsed = yaml.load(result.content[0].text) as Record; + + expect(parsed).toHaveProperty('academy'); + expect(parsed).toHaveProperty('authoringPlan'); + expect(parsed.authoringPlan.sourceOfTruth.requiredBeforeGraphWork).toBe(true); + expect(parsed.parts.map((part: any) => part.id)).toEqual([ + 'foundations', + 'core-structures', + 'operational-flows', + 'applied-judgment', + ]); + expect(parsed.courses).toHaveLength(4); + }); +}); + describe('graspful_validate', () => { test('valid course passes validation', async () => { const courseYaml = await scaffoldCourse('Validation Test'); diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index c39966a..92ca4db 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -113,9 +113,9 @@ interface ToolDef { const TOOLS: ToolDef[] = [ { name: 'graspful_create_academy', - description: `Generate an academy manifest scaffold for an academy-first workflow. Every academy is a connected curriculum made of one or more real courses. + description: `Generate an academy plan and manifest scaffold for an academy-first workflow. Every academy is a connected curriculum made of one or more real courses. -Use this before authoring course YAML when the topic should be decomposed into multiple learner-facing parts. If you do not pass courseNames, the scaffold creates a single foundations course so the academy can still grow later without changing the outer product shape.`, +Use this before authoring course YAML when the topic should be decomposed into learner-facing parts. If you do not pass courseNames, the scaffold creates the four default planning layers: foundations, core structures, operational flows, and applied judgment. The result includes authoring gates for source material, learner promise, landing-page proof, graph checks, and review before publishing.`, inputSchema: { type: 'object', properties: { diff --git a/packages/shared/src/__tests__/domain.test.ts b/packages/shared/src/__tests__/domain.test.ts index b55449b..2c99f27 100644 --- a/packages/shared/src/__tests__/domain.test.ts +++ b/packages/shared/src/__tests__/domain.test.ts @@ -168,12 +168,20 @@ describe('scaffoldCourseObject', () => { }); describe('scaffoldAcademyObject', () => { - it('scaffolds an academy manifest with one default course', () => { + it('scaffolds an academy manifest with the default academy layers', () => { const result = scaffoldAcademyObject('PostHog TAM'); expect(result.academy.id).toBe('posthog-tam'); expect(result.academy.name).toBe('PostHog TAM Academy'); - expect(result.courses).toHaveLength(1); + expect(result.authoringPlan.sourceOfTruth.requiredBeforeGraphWork).toBe(true); + expect(result.parts.map((part) => part.id)).toEqual([ + 'foundations', + 'core-structures', + 'operational-flows', + 'applied-judgment', + ]); + expect(result.courses).toHaveLength(4); expect(result.courses[0].file).toBe('courses/posthog-tam-foundations.yaml'); + expect(result.courses[3].part).toBe('applied-judgment'); }); it('uses provided course names when present', () => { @@ -182,7 +190,9 @@ describe('scaffoldAcademyObject', () => { }); expect(result.courses).toHaveLength(2); expect(result.courses[0].id).toBe('data-models'); + expect(result.courses[0].part).toBe('foundations'); expect(result.courses[1].id).toBe('data-pipelines'); + expect(result.courses[1].part).toBe('core-structures'); }); }); diff --git a/packages/shared/src/scaffold.ts b/packages/shared/src/scaffold.ts index 0a9da14..505214f 100644 --- a/packages/shared/src/scaffold.ts +++ b/packages/shared/src/scaffold.ts @@ -43,10 +43,37 @@ export function scaffoldAcademyObject( options: { courseNames?: string[]; version?: string } = {}, ) { const academySlug = slugify(topic); + const layerTemplates = [ + { + id: 'foundations', + name: 'Foundations', + description: 'Concepts that must become automatic before the rest of the academy can work.', + defaultCourseName: `${topic} Foundations`, + }, + { + id: 'core-structures', + name: 'Core Structures', + description: 'The organizing models, systems, and relationships that later work depends on.', + defaultCourseName: `${topic} Core Structures`, + }, + { + id: 'operational-flows', + name: 'Operational Flows', + description: 'How learners use the core structures in realistic workflows.', + defaultCourseName: `${topic} Operational Flows`, + }, + { + id: 'applied-judgment', + name: 'Applied Judgment', + description: 'Scenario work where learners diagnose, choose, adapt, and transfer knowledge.', + defaultCourseName: `${topic} Applied Judgment`, + }, + ]; + const courseNames = options.courseNames && options.courseNames.length > 0 ? options.courseNames - : [`${topic} Foundations`]; + : layerTemplates.map((layer) => layer.defaultCourseName); return { academy: { @@ -55,12 +82,52 @@ export function scaffoldAcademyObject( description: `Adaptive academy for ${topic}. Break the domain into connected courses that build from foundations to applied performance.`, version: options.version || '2026.1', }, - courses: courseNames.map((courseName) => { + authoringPlan: { + status: 'draft', + sourceOfTruth: { + requiredBeforeGraphWork: true, + expectedInputs: [ + 'Official exam blueprint, syllabus, handbook, standard, codebase, or curriculum outline', + 'Official learning objectives or competency statements', + 'Edition, version, or date of the source material', + 'Scope boundary: what is in scope and what is explicitly out of scope', + ], + }, + learnerPromise: { + who: 'TODO: Name the exact learner or buyer persona', + outcome: 'TODO: Name the concrete outcome they want', + whyHardToday: 'TODO: Explain why generic content does not solve this well', + advantage: 'TODO: Explain what this academy teaches better than generic content', + proof: 'TODO: Point to source material, examples, curriculum shape, or domain proof', + }, + landingPage: { + headline: 'TODO: Write a learner-facing headline tied to the real outcome', + subheadline: 'TODO: Explain the academy structure and why it builds competence', + proofPoints: [ + 'TODO: Name a concrete course, section, source, example, or assessment the page can point to', + ], + }, + graphChecklist: [ + 'Draft the academy course dependency map before authoring any knowledge points', + 'Write course YAML skeletons before filling concept content', + 'Use cross-course prerequisites when one course depends on another', + 'Check that later courses reinforce earlier foundations instead of leaving them behind', + 'Run validate after each structural edit and review before publishing', + ], + }, + parts: layerTemplates.map(({ id, name, description }) => ({ + id, + name, + description, + })), + courses: courseNames.map((courseName, index) => { const courseSlug = slugify(courseName); + const part = layerTemplates[Math.min(index, layerTemplates.length - 1)].id; return { id: courseSlug, name: courseName, description: `Course in the ${topic} academy covering ${courseName.toLowerCase()}.`, + part, file: `courses/${courseSlug}.yaml`, }; }),