Plan: Automated Content Scaffolding — Value Streams & Management Practices

Version: 1.1 Date: 2026-02-17 Status: Implemented Related:


1. Executive Summary

This plan introduces a GitHub-native governance automation system that transforms the creation of Value Streams and Management Practices from a manual, error-prone process into a structured, validated, and largely automated workflow. The entry point is a GitHub Issue Form (structured YAML template); the exit point is a fully scaffolded Pull Request with all files, folder structures, frontmatter, cross-references, CODEOWNERS entries, and an AI-powered MECE analysis — ready for human review.

Key Benefits:

  • Zero manual scaffolding — folder trees, stub files, frontmatter, and cross-references are generated automatically from structured issue inputs
  • Governance enforcement — deterministic validation catches naming violations, code collisions, and structural errors before any human reviews
  • MECE integrity — AI-powered semantic overlap detection compares the proposed VS/Practice purpose against all existing ones, flagging potential boundary conflicts
  • CODEOWNERS readiness — the automation scaffolds CODEOWNERS entries proactively, with warnings when the referenced GitHub Team doesn’t yet exist
  • Audit trail — every VS/Practice creation is traced from issue → PR → merge, with the full validation report embedded in the PR body

Critical Dependencies:

  • GitHub Actions workflows with issues and pull_request event triggers
  • GitHub Models API access for AI-powered MECE checks (free for Actions)
  • Node.js 22 runtime (already configured in deploy.yml)
  • Existing repository content structure (content/01 Value Streams/, content/02 Guilds/)

Scope: Value Stream and Management Practice creation only. Guild creation, Product creation, and decision scaffolding are out of scope and may be addressed in future plans.


2. Architecture / Context Overview

Current State

AspectCurrent Behavior
VS/Practice creationManual: copy template from content/metadata/templater/, rename files, populate frontmatter, create folders, submit PR
Structural validationNone — reviewers manually check folder structure, naming, frontmatter
MECE checkingManual — author and reviewer assess overlap by reading existing content
CODEOWNERSDeferred — no file exists; planned structure documented in docs/CODEOWNERS_DEFERRED.md
Issue trackingNo issue templates — creation requests are ad-hoc
CI validationNone for content — only deploy.yml builds the site on push to main

Proposed State

AspectProposed Behavior
VS/Practice creationAutomated: fill GitHub Issue Form → Actions workflow scaffolds all files → PR created
Structural validationDeterministic: Node.js scripts validate naming, codes, folder structure, cross-references
MECE checkingAI-assisted: GitHub Models API compares proposed purpose against all existing ones
CODEOWNERSAuto-managed: scaffolding workflow appends entries, warns if team doesn’t exist
Issue trackingStructured YAML issue forms with validation fields and auto-labels
CI validationContent validation workflow runs on all content/** PRs

Architecture Diagram

                    ┌──────────────────────────┐
                    │   GitHub Issue Form       │
                    │  (YAML Issue Template)    │
                    └───────────┬──────────────┘
                                │ issues.opened + label
                                ▼
                    ┌──────────────────────────┐
                    │  GitHub Actions Workflow  │
                    │                           │
                    │  ┌─ parse-issue.mjs       │
                    │  ├─ validate-structure.mjs│
                    │  ├─ mece-check.mjs        │
                    │  ├─ scaffold-files.mjs    │
                    │  └─ update-codeowners.mjs │
                    └───────────┬──────────────┘
                                │
                                ▼
                    ┌──────────────────────────┐
                    │  Pull Request             │
                    │  - Scaffolded files        │
                    │  - Validation report       │
                    │  - MECE analysis           │
                    │  - CODEOWNERS update       │
                    │  - Linked to issue         │
                    └──────────────────────────┘
                                │
                                ▼
                    ┌──────────────────────────┐
                    │  validate-content.yml     │
                    │  (CI gate on all PRs)     │
                    └──────────────────────────┘

Key Tradeoffs

DecisionChoiceRationale
Entry pointGitHub Issue FormsCentralizes governance, creates audit trail, accessible to non-technical users, no local tooling required
Template ownershipScripts own templates (inline strings)Full control over output; no Templater syntax parsing; single source of truth; supersedes TMP01/TMP03
AI MECE modeAdvisory (not blocking)Semantic overlap is inherently fuzzy; humans make the final call; AI provides evidence
Script languageNode.js ESM (.mjs)Matches existing Quartz/Node.js stack and "type": "module" in package.json
CODEOWNERS timingScaffold proactively with warningsEntries are ready to activate when GitHub Teams are created; warnings prevent silent failures
Issue-to-PR flowFully automated (no manual trigger)Minimizes friction; the PR itself is the review/approval gate

3. Implementation Steps

Step 1: Create Issue Body Parser

Goal: A reusable utility that extracts structured field values from GitHub Issue Form markdown bodies.

File: scripts/parse-issue.mjs

GitHub Issue Forms produce markdown with predictable ### FieldName\n\nValue patterns. This script parses those into a structured object.

// scripts/parse-issue.mjs
// Parses GitHub Issue Form body into structured key-value object.
//
// GitHub Issue Forms emit markdown in this pattern:
//   ### Field Label
//
//   User's value
//
//   ### Next Field Label
//
//   Next value
 
/**
 * @param {string} body - Raw issue body markdown
 * @returns {Record<string, string>} - Map of field label → trimmed value
 */
export function parseIssueBody(body) {
  const fields = {};
  const sections = body.split(/^### /m).filter(Boolean);
 
  for (const section of sections) {
    const newlineIndex = section.indexOf("\n");
    if (newlineIndex === -1) continue;
 
    const label = section.slice(0, newlineIndex).trim();
    const value = section.slice(newlineIndex + 1).trim();
 
    // Skip "_No response_" default for optional fields
    if (value && value !== "_No response_") {
      fields[label] = value;
    }
  }
 
  return fields;
}
 
/**
 * Converts a field value containing a comma-separated or newline-separated
 * list into an array of trimmed, non-empty strings.
 * @param {string} value
 * @returns {string[]}
 */
export function parseList(value) {
  if (!value) return [];
  return value
    .split(/[,\n]/)
    .map((item) => item.replace(/^[-*]\s*/, "").trim())
    .filter(Boolean);
}
 
/**
 * Extracts the guild code (e.g., "GL03") and full guild folder name
 * (e.g., "GL03 Delivery Guild") from a dropdown selection.
 * @param {string} selection - e.g., "GL03 Delivery Guild"
 * @returns {{ code: string, folderName: string }}
 */
export function parseGuildSelection(selection) {
  const match = selection.match(/^(GL\d{2})\s+(.+)$/);
  if (!match) throw new Error(`Invalid guild selection: "${selection}"`);
  return { code: match[1], folderName: selection.trim() };
}

Verification: Unit test with sample issue body markdown (see Step 9).


Step 2: Create Structural Validation Script

Goal: Deterministic validation of naming conventions, code uniqueness, folder collisions, and cross-reference integrity.

File: scripts/validate-structure.mjs

// scripts/validate-structure.mjs
import { readdir, readFile, stat } from "node:fs/promises";
import { join, resolve } from "node:path";
import matter from "gray-matter";
 
const CONTENT_DIR = resolve(process.cwd(), "content");
const VS_DIR = join(CONTENT_DIR, "01 Value Streams");
const GUILDS_DIR = join(CONTENT_DIR, "02 Guilds");
 
/**
 * Scans content/01 Value Streams/ for existing VS codes.
 * @returns {Promise<string[]>} e.g., ["VS01", "VS02", ...]
 */
export async function getExistingVSCodes() {
  const entries = await readdir(VS_DIR, { withFileTypes: true });
  return entries
    .filter((e) => e.isDirectory() && /^VS\d{2}\s/.test(e.name))
    .map((e) => e.name.match(/^(VS\d{2})/)[1])
    .sort();
}
 
/**
 * Scans all practice README.md files for existing practice IDs.
 * @returns {Promise<Array<{ id: string, guild: string, name: string }>>}
 */
export async function getExistingPracticeIds() {
  const results = [];
  const guilds = await readdir(GUILDS_DIR, { withFileTypes: true });
 
  for (const guild of guilds.filter(
    (g) => g.isDirectory() && /^GL\d{2}/.test(g.name),
  )) {
    const practicesDir = join(GUILDS_DIR, guild.name, "Practices");
    try {
      const practices = await readdir(practicesDir, { withFileTypes: true });
      for (const practice of practices.filter((p) => p.isDirectory())) {
        const overviewPath = join(practicesDir, practice.name, "README.md");
        try {
          const content = await readFile(overviewPath, "utf-8");
          const { data } = matter(content);
          if (data["practice-id"]) {
            results.push({
              id: data["practice-id"],
              guild: guild.name,
              name: practice.name,
            });
          }
        } catch {
          // No overview file — skip
        }
      }
    } catch {
      // No Practices directory — skip
    }
  }
 
  return results;
}
 
/**
 * Validates a VS code for format and uniqueness.
 * @returns {Promise<{ valid: boolean, errors: string[] }>}
 */
export async function validateVSCode(code) {
  const errors = [];
  if (!/^VS\d{2}$/.test(code)) {
    errors.push(
      `VS code "${code}" does not match required format VS## (e.g., VS08)`,
    );
  }
  const existing = await getExistingVSCodes();
  if (existing.includes(code)) {
    errors.push(
      `VS code "${code}" already exists. Existing codes: ${existing.join(", ")}`,
    );
  }
  return { valid: errors.length === 0, errors };
}
 
/**
 * Validates a practice ID for format and uniqueness.
 * @returns {Promise<{ valid: boolean, errors: string[] }>}
 */
export async function validatePracticeId(id) {
  const errors = [];
  if (!/^[A-Z]{2,5}-[A-Z]{2,5}-\d{2}$/.test(id)) {
    errors.push(
      `Practice ID "${id}" does not match required format [A-Z]{2,5}-[A-Z]{2,5}-## (e.g., DEL-CHG-01)`,
    );
  }
  const existing = await getExistingPracticeIds();
  const collision = existing.find((e) => e.id === id);
  if (collision) {
    errors.push(
      `Practice ID "${id}" already exists in ${collision.guild} / ${collision.name}`,
    );
  }
  return { valid: errors.length === 0, errors };
}
 
/**
 * Validates naming conventions for a VS or Practice name.
 * @param {string} name
 * @param {"value-stream" | "practice"} type
 * @returns {{ valid: boolean, errors: string[] }}
 */
export function validateNamingConventions(name, type) {
  const errors = [];
 
  // No special characters except hyphens and spaces
  if (/[^a-zA-Z0-9\s\-]/.test(name)) {
    errors.push(
      `Name "${name}" contains special characters. Only letters, numbers, spaces, and hyphens are allowed.`,
    );
  }
 
  // Practice names must end with "Management"
  if (type === "practice" && !name.endsWith("Management")) {
    errors.push(
      `Practice name "${name}" must end with "Management" (e.g., "Change Management")`,
    );
  }
 
  // Title Case check (first letter of each word capitalised)
  const words = name.split(/\s+/);
  const nonTitleCase = words.filter(
    (w) => w.length > 0 && w[0] !== w[0].toUpperCase(),
  );
  if (nonTitleCase.length > 0) {
    errors.push(
      `Name "${name}" should be Title Case. These words need capitalising: ${nonTitleCase.join(", ")}`,
    );
  }
 
  return { valid: errors.length === 0, errors };
}
 
/**
 * Checks whether a folder path already exists (collision detection).
 * @param {string} relativePath - Path relative to content/
 * @returns {Promise<{ exists: boolean }>}
 */
export async function checkFolderCollision(relativePath) {
  const fullPath = join(CONTENT_DIR, relativePath);
  try {
    const s = await stat(fullPath);
    return { exists: s.isDirectory() };
  } catch {
    return { exists: false };
  }
}
 
/**
 * Validates that a guild folder exists.
 * @param {string} guildFolderName - e.g., "GL03 Delivery Guild"
 * @returns {Promise<{ valid: boolean, errors: string[] }>}
 */
export async function validateGuildExists(guildFolderName) {
  const errors = [];
  const fullPath = join(GUILDS_DIR, guildFolderName);
  try {
    await stat(fullPath);
  } catch {
    errors.push(
      `Guild folder "${guildFolderName}" does not exist in content/02 Guilds/`,
    );
  }
  return { valid: errors.length === 0, errors };
}
 
/**
 * Validates that referenced practices exist somewhere in the guild tree.
 * @param {string[]} practiceNames
 * @returns {Promise<{ valid: boolean, found: string[], missing: string[] }>}
 */
export async function validatePracticesExist(practiceNames) {
  const found = [];
  const missing = [];
 
  for (const name of practiceNames) {
    let exists = false;
    const guilds = await readdir(GUILDS_DIR, { withFileTypes: true });
    for (const guild of guilds.filter(
      (g) => g.isDirectory() && /^GL\d{2}/.test(g.name),
    )) {
      const practicesDir = join(GUILDS_DIR, guild.name, "Practices");
      try {
        const practices = await readdir(practicesDir, { withFileTypes: true });
        if (practices.some((p) => p.isDirectory() && p.name === name)) {
          exists = true;
          break;
        }
      } catch {
        // No Practices directory — skip
      }
    }
    (exists ? found : missing).push(name);
  }
 
  return { valid: missing.length === 0, found, missing };
}
 
/**
 * Suggests the next available VS code.
 * @returns {Promise<string>}
 */
export async function suggestNextVSCode() {
  const existing = await getExistingVSCodes();
  if (existing.length === 0) return "VS01";
  const maxNum = Math.max(
    ...existing.map((c) => parseInt(c.replace("VS", ""), 10)),
  );
  return `VS${String(maxNum + 1).padStart(2, "0")}`;
}
 
/**
 * Runs all validations for a Value Stream creation request.
 * @param {{ code: string, name: string, relatedPractices: string[] }} inputs
 * @returns {Promise<{ passed: boolean, results: Array<{ check: string, passed: boolean, detail: string }> }>}
 */
export async function validateValueStream(inputs) {
  const results = [];
 
  // Code format + uniqueness
  const codeResult = await validateVSCode(inputs.code);
  results.push({
    check: "VS Code Format & Uniqueness",
    passed: codeResult.valid,
    detail: codeResult.valid
      ? `${inputs.code} is valid and unique`
      : codeResult.errors.join("; "),
  });
 
  // Naming conventions
  const nameResult = validateNamingConventions(inputs.name, "value-stream");
  results.push({
    check: "Naming Convention",
    passed: nameResult.valid,
    detail: nameResult.valid
      ? `"${inputs.name}" follows naming conventions`
      : nameResult.errors.join("; "),
  });
 
  // Folder collision
  const folderPath = `01 Value Streams/${inputs.code} ${inputs.name}`;
  const collision = await checkFolderCollision(folderPath);
  results.push({
    check: "No Folder Collision",
    passed: !collision.exists,
    detail: collision.exists
      ? `Folder "content/${folderPath}" already exists`
      : `Path "content/${folderPath}" is available`,
  });
 
  // Related practices exist
  if (inputs.relatedPractices.length > 0) {
    const practicesResult = await validatePracticesExist(
      inputs.relatedPractices,
    );
    results.push({
      check: "Related Practices Exist",
      passed: practicesResult.valid,
      detail: practicesResult.valid
        ? `All ${practicesResult.found.length} referenced practices found`
        : `Missing practices: ${practicesResult.missing.join(", ")}`,
    });
  } else {
    results.push({
      check: "Related Practices Exist",
      passed: true,
      detail: "No related practices specified (optional field)",
    });
  }
 
  return {
    passed: results.every((r) => r.passed),
    results,
  };
}
 
/**
 * Runs all validations for a Management Practice creation request.
 * @param {{ id: string, name: string, guildFolder: string, dependencies: string[], dependents: string[] }} inputs
 * @returns {Promise<{ passed: boolean, results: Array<{ check: string, passed: boolean, detail: string }> }>}
 */
export async function validatePractice(inputs) {
  const results = [];
 
  // Practice ID format + uniqueness
  const idResult = await validatePracticeId(inputs.id);
  results.push({
    check: "Practice ID Format & Uniqueness",
    passed: idResult.valid,
    detail: idResult.valid
      ? `${inputs.id} is valid and unique`
      : idResult.errors.join("; "),
  });
 
  // Naming conventions
  const nameResult = validateNamingConventions(inputs.name, "practice");
  results.push({
    check: "Naming Convention",
    passed: nameResult.valid,
    detail: nameResult.valid
      ? `"${inputs.name}" follows naming conventions`
      : nameResult.errors.join("; "),
  });
 
  // Parent guild exists
  const guildResult = await validateGuildExists(inputs.guildFolder);
  results.push({
    check: "Parent Guild Exists",
    passed: guildResult.valid,
    detail: guildResult.valid
      ? `Guild folder "${inputs.guildFolder}" exists`
      : guildResult.errors.join("; "),
  });
 
  // Folder collision
  const folderPath = `02 Guilds/${inputs.guildFolder}/Practices/${inputs.name}`;
  const collision = await checkFolderCollision(folderPath);
  results.push({
    check: "No Folder Collision",
    passed: !collision.exists,
    detail: collision.exists
      ? `Folder "content/${folderPath}" already exists`
      : `Path "content/${folderPath}" is available`,
  });
 
  // Dependencies exist
  const allRefs = [...inputs.dependencies, ...inputs.dependents];
  if (allRefs.length > 0) {
    const refsResult = await validatePracticesExist(allRefs);
    results.push({
      check: "Referenced Practices Exist",
      passed: refsResult.valid,
      detail: refsResult.valid
        ? `All ${refsResult.found.length} referenced practices found`
        : `Missing practices: ${refsResult.missing.join(", ")}`,
    });
  } else {
    results.push({
      check: "Referenced Practices Exist",
      passed: true,
      detail: "No dependencies/dependents specified (optional fields)",
    });
  }
 
  return {
    passed: results.every((r) => r.passed),
    results,
  };
}

Dependencies: gray-matter (already in Quartz’s dependency tree — verify with npm ls gray-matter; if not present, add as a dev dependency from a Windows terminal).

Verification: Run unit tests (Step 9); manually run against known VS codes and practice IDs:

node -e "import('./scripts/validate-structure.mjs').then(m => m.getExistingVSCodes().then(console.log))"
# Expected: ["VS01", "VS02", "VS03", "VS04", "VS05", "VS06", "VS07"]

Step 3: Create File Scaffolding Script

Goal: Generate the complete folder tree and populated markdown files for a new Value Stream or Management Practice. This script is the source of truth for templates, superseding the Obsidian Templater files TMP01 and TMP03.

File: scripts/scaffold-files.mjs

// scripts/scaffold-files.mjs
import { mkdir, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
 
const CONTENT_DIR = resolve(process.cwd(), "content");
 
/**
 * Creates a directory and all parents. Writes a .gitkeep if the directory
 * is intended to be empty (git doesn't track empty directories).
 */
async function ensureDir(dirPath, { gitkeep = false } = {}) {
  await mkdir(dirPath, { recursive: true });
  if (gitkeep) {
    await writeFile(join(dirPath, ".gitkeep"), "");
  }
}
 
// ─── Value Stream Templates ────────────────────────────────────────────────
 
function vsOverviewTemplate(inputs) {
  const today = new Date().toISOString().slice(0, 10);
  return `---
publish: true
page-status: draft
created: ${today}
owner: ${inputs.ownerSlug}
vs-code: ${inputs.code}
type: value-stream-overview
---
 
# ${inputs.code} ${inputs.name}
 
## Purpose and Outcomes
 
${inputs.purpose}
 
## Customers and Stakeholders
 
### Primary Customer
 
${inputs.primaryCustomer}
 
### Key Stakeholders
 
${inputs.stakeholders || "- [Stakeholder] - [Their interest]"}
 
## Boundaries
 
### Start Trigger
 
${inputs.startTrigger}
 
### End Trigger
 
${inputs.endTrigger}
 
### Typical Duration
 
${inputs.typicalDuration || "[To be determined]"}
 
## Owner and Governance Model
 
- **Value Stream Owner:** ${inputs.owner}
- **Governance Team:** \`@calab-ai/${inputs.teamSlug}\`
- **Decision Authority:** [To be defined]
- **Escalation Path:** [To be defined]
 
## Related Practices
 
This Value Stream coordinates the following practices (see [[05 Practice Links]] for detailed mappings):
 
${inputs.relatedPractices.map((p) => `- [[${p}]]`).join("\n") || "- [No practices linked yet]"}
 
**Note:** This Value Stream does NOT own practice artefacts. All policies, processes, and procedures remain owned by their respective practices.
 
## Value Stream Documentation
 
- [[01 Value Stream Map]] - Visual flow and stages
- [[02 Governance & Roles]] - RACI and decision rights
- [[03 Metrics]] - Performance indicators and targets
- [[04 Interfaces]] - Dependencies and handoffs
- [[05 Practice Links]] - Detailed practice relationships
 
## Review Cadence
 
- **VS Performance Review:** Monthly
- **VS Design Review:** Quarterly
- **Owner:** ${inputs.owner}
 
## Change Log
 
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 1.0 | ${today} | ${inputs.author || "[Author]"} | Initial creation (auto-scaffolded) |
`;
}
 
function vsStubTemplate(title, pageStatus = "draft") {
  const today = new Date().toISOString().slice(0, 10);
  return `---
page-status: ${pageStatus}
created: ${today}
---
 
# ${title}
 
[To be completed — this page was auto-scaffolded. See the [[README]] for context.]
`;
}
 
function vsPracticeLinksTemplate(inputs) {
  const today = new Date().toISOString().slice(0, 10);
  return `---
page-status: draft
created: ${today}
---
 
# Practice Links
 
This document maps the management practices that participate in the **${inputs.code} ${inputs.name}** value stream.
 
## Coordinated Practices
 
${inputs.relatedPractices.length > 0 ? inputs.relatedPractices.map((p) => `### ${p}\n\n- **Role in this VS:** [To be defined]\n- **Key handoffs:** [To be defined]\n- **Interfaces:** [To be defined]\n`).join("\n") : "[No practices linked yet — update this page as practice relationships are established.]"}
 
## Practice Relationship Principles
 
- Value Streams coordinate practices but do NOT own practice artefacts
- Practice artefacts (policies, processes, procedures) remain owned by their respective Guilds
- This mapping defines how practices contribute to end-to-end value delivery
`;
}
 
/**
 * Scaffolds a complete Value Stream folder structure.
 * @param {object} inputs
 * @param {string} inputs.code - e.g., "VS08"
 * @param {string} inputs.name - e.g., "Plan to Innovate"
 * @param {string} inputs.purpose - 2-3 sentence purpose description
 * @param {string} inputs.primaryCustomer
 * @param {string} inputs.stakeholders - Raw stakeholder text
 * @param {string} inputs.startTrigger
 * @param {string} inputs.endTrigger
 * @param {string} inputs.typicalDuration
 * @param {string} inputs.owner - VS Owner name/role
 * @param {string} inputs.ownerSlug - Frontmatter owner slug
 * @param {string} inputs.teamSlug - GitHub team slug
 * @param {string[]} inputs.relatedPractices - Practice names
 * @param {string} [inputs.author] - Author name for change log
 * @returns {Promise<string[]>} - List of created file paths (relative to repo root)
 */
export async function scaffoldValueStream(inputs) {
  const folderName = `${inputs.code} ${inputs.name}`;
  const vsDir = join(CONTENT_DIR, "01 Value Streams", folderName);
  const createdFiles = [];
 
  // Create directory structure
  await ensureDir(vsDir);
  await ensureDir(join(vsDir, "metadata", "diagrams"), { gitkeep: true });
  await ensureDir(join(vsDir, "metadata", "plans"), { gitkeep: true });
  await ensureDir(join(vsDir, "metadata", "specs"), { gitkeep: true });
 
  // Write files
  const files = [
    ["README.md", vsOverviewTemplate(inputs)],
    ["01 Value Stream Map.md", vsStubTemplate("Value Stream Map")],
    ["02 Governance & Roles.md", vsStubTemplate("Governance & Roles")],
    ["03 Metrics.md", vsStubTemplate("Metrics")],
    ["04 Interfaces.md", vsStubTemplate("Interfaces")],
    ["05 Practice Links.md", vsPracticeLinksTemplate(inputs)],
  ];
 
  for (const [filename, content] of files) {
    const filePath = join(vsDir, filename);
    await writeFile(filePath, content, "utf-8");
    createdFiles.push(`content/01 Value Streams/${folderName}/${filename}`);
  }
 
  // Track .gitkeep files
  createdFiles.push(
    `content/01 Value Streams/${folderName}/metadata/diagrams/.gitkeep`,
    `content/01 Value Streams/${folderName}/metadata/plans/.gitkeep`,
    `content/01 Value Streams/${folderName}/metadata/specs/.gitkeep`,
  );
 
  return createdFiles;
}
 
// ─── Management Practice Templates ────────────────────────────────────────
 
function practiceOverviewTemplate(inputs) {
  const today = new Date().toISOString().slice(0, 10);
  return `---
page-status: draft
created: ${today}
owner: ${inputs.ownerSlug}
practice-id: ${inputs.id}
guild: ${inputs.guildFolder}
type: practice-overview
---
 
# ${inputs.name}
 
## Metadata
 
- **Practice ID:** ${inputs.id}
- **Status:** Draft
- **Version:** 1.0
- **Owner Role:** ${inputs.ownerRole}
- **Guild:** [[${inputs.guildFolder}]]
 
## Purpose and Objectives
 
${inputs.purpose}
 
## Scope
 
### In Scope
 
${inputs.inScope || "- [To be defined]"}
 
### Out of Scope
 
${inputs.outOfScope || "- [To be defined]"}
 
## Interfaces
 
### Dependencies (Practices We Depend On)
 
${inputs.dependencies.length > 0 ? inputs.dependencies.map((p) => `- [[${p}]] - [Relationship to be defined]`).join("\n") : "- [No dependencies identified yet]"}
 
### Dependents (Practices That Depend On Us)
 
${inputs.dependents.length > 0 ? inputs.dependents.map((p) => `- [[${p}]] - [Relationship to be defined]`).join("\n") : "- [No dependents identified yet]"}
 
## Related Practices and Resources
 
### Related Practices
 
- [To be defined]
 
### Key Processes
 
- [To be defined — add links to documents in 02 Processes/]
 
### Key Templates
 
- [To be defined — add links to documents in 05 Templates/]
 
## KPIs and Success Signals
 
- **KPI 1:** [Metric and target]
- **KPI 2:** [Metric and target]
- **Success Signal 1:** [Qualitative indicator]
- **Success Signal 2:** [Qualitative indicator]
 
## Review Cadence
 
- **Practice Review:** Monthly
- **Artefact Review:** Quarterly
- **Owner:** ${inputs.ownerRole}
 
## Change Log
 
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 1.0 | ${today} | ${inputs.author || "[Author]"} | Initial creation (auto-scaffolded) |
`;
}
 
/**
 * Scaffolds a complete Management Practice folder structure.
 * @param {object} inputs
 * @param {string} inputs.name - e.g., "Change Management"
 * @param {string} inputs.id - e.g., "DEL-CHG-01"
 * @param {string} inputs.guildFolder - e.g., "GL03 Delivery Guild"
 * @param {string} inputs.purpose - 2-3 sentence purpose
 * @param {string} inputs.inScope - Raw in-scope text
 * @param {string} inputs.outOfScope - Raw out-of-scope text
 * @param {string} inputs.ownerRole - e.g., "Change Management Lead"
 * @param {string} inputs.ownerSlug - Frontmatter owner slug
 * @param {string[]} inputs.dependencies - Practice names
 * @param {string[]} inputs.dependents - Practice names
 * @param {string} [inputs.author] - Author name for change log
 * @returns {Promise<string[]>} - List of created file paths (relative to repo root)
 */
export async function scaffoldPractice(inputs) {
  const practiceDir = join(
    CONTENT_DIR,
    "02 Guilds",
    inputs.guildFolder,
    "Practices",
    inputs.name,
  );
  const createdFiles = [];
  const relPath = `content/02 Guilds/${inputs.guildFolder}/Practices/${inputs.name}`;
 
  // Create directory structure
  await ensureDir(practiceDir);
  const artefactDirs = [
    "01 Policies",
    "02 Processes",
    "03 Procedures",
    "04 Guides",
    "05 Templates",
  ];
  for (const dir of artefactDirs) {
    await ensureDir(join(practiceDir, dir), { gitkeep: true });
    createdFiles.push(`${relPath}/${dir}/.gitkeep`);
  }
  await ensureDir(join(practiceDir, "metadata", "diagrams"), { gitkeep: true });
  await ensureDir(join(practiceDir, "metadata", "plans"), { gitkeep: true });
  await ensureDir(join(practiceDir, "metadata", "specs"), { gitkeep: true });
  createdFiles.push(
    `${relPath}/metadata/diagrams/.gitkeep`,
    `${relPath}/metadata/plans/.gitkeep`,
    `${relPath}/metadata/specs/.gitkeep`,
  );
 
  // Write overview
  const overviewPath = join(practiceDir, "README.md");
  await writeFile(overviewPath, practiceOverviewTemplate(inputs), "utf-8");
  createdFiles.unshift(`${relPath}/README.md`);
 
  return createdFiles;
}

Verification: Run scaffolding in a temp directory and inspect output structure:

# Dry-run test (uses temp dir, not actual content/)
node -e "
  import { scaffoldValueStream } from './scripts/scaffold-files.mjs';
  const files = await scaffoldValueStream({
    code: 'VS99', name: 'Test Stream', purpose: 'Test purpose.',
    primaryCustomer: 'Test customer', stakeholders: '',
    startTrigger: 'Test trigger', endTrigger: 'Test end',
    typicalDuration: '1 week', owner: 'Test Owner',
    ownerSlug: 'test-owner', teamSlug: 'test-team',
    relatedPractices: ['Project Mgmt'], author: 'Test'
  });
  console.log(files);
"

Step 4: Create MECE Check Script

Goal: AI-powered semantic overlap detection using the GitHub Models API.

File: scripts/mece-check.mjs

// scripts/mece-check.mjs
import { readdir, readFile } from "node:fs/promises";
import { join, resolve } from "node:path";
 
const CONTENT_DIR = resolve(process.cwd(), "content");
const VS_DIR = join(CONTENT_DIR, "01 Value Streams");
const GUILDS_DIR = join(CONTENT_DIR, "02 Guilds");
 
/**
 * Extracts the "Purpose" section from a markdown file.
 * Looks for ## Purpose, ## Purpose and Outcomes, or ## Purpose and Objectives.
 * Returns the text until the next ## heading.
 */
function extractPurposeSection(markdown) {
  const match = markdown.match(
    /^##\s+Purpose(?:\s+and\s+(?:Outcomes|Objectives))?\s*\n([\s\S]*?)(?=\n##\s|\n---\s*$|$)/m,
  );
  return match ? match[1].trim() : "";
}
 
/**
 * Collects purpose descriptions from all existing Value Stream overviews.
 * @returns {Promise<Array<{ code: string, name: string, purpose: string }>>}
 */
export async function collectExistingVSPurposes() {
  const results = [];
  const entries = await readdir(VS_DIR, { withFileTypes: true });
 
  for (const entry of entries.filter(
    (e) => e.isDirectory() && /^VS\d{2}/.test(e.name),
  )) {
    const overviewPath = join(VS_DIR, entry.name, "README.md");
    try {
      const content = await readFile(overviewPath, "utf-8");
      const purpose = extractPurposeSection(content);
      const codeMatch = entry.name.match(/^(VS\d{2})\s+(.+)$/);
      if (codeMatch && purpose) {
        results.push({ code: codeMatch[1], name: codeMatch[2], purpose });
      }
    } catch {
      // No overview — skip
    }
  }
 
  return results;
}
 
/**
 * Collects purpose descriptions from all existing Practice overviews.
 * @param {string} [guildFilter] - Optional guild folder name to filter by
 * @returns {Promise<Array<{ guild: string, name: string, purpose: string }>>}
 */
export async function collectExistingPracticePurposes(guildFilter) {
  const results = [];
  const guilds = await readdir(GUILDS_DIR, { withFileTypes: true });
 
  for (const guild of guilds.filter(
    (g) => g.isDirectory() && /^GL\d{2}/.test(g.name),
  )) {
    if (guildFilter && guild.name !== guildFilter) continue;
 
    const practicesDir = join(GUILDS_DIR, guild.name, "Practices");
    try {
      const practices = await readdir(practicesDir, { withFileTypes: true });
      for (const practice of practices.filter((p) => p.isDirectory())) {
        const overviewPath = join(practicesDir, practice.name, "README.md");
        try {
          const content = await readFile(overviewPath, "utf-8");
          const purpose = extractPurposeSection(content);
          if (purpose) {
            results.push({ guild: guild.name, name: practice.name, purpose });
          }
        } catch {
          // No overview — skip
        }
      }
    } catch {
      // No Practices directory — skip
    }
  }
 
  return results;
}
 
/**
 * Calls the GitHub Models API to perform MECE overlap analysis.
 *
 * @param {string} proposedName
 * @param {string} proposedPurpose
 * @param {Array<{ name: string, purpose: string }>} existing
 * @param {"value-stream" | "practice"} type
 * @param {string} token - GitHub token with models API access
 * @returns {Promise<{ overlapRisk: string, concerns: string[], recommendation: string, comparedAgainst: string[] }>}
 */
export async function checkMECEOverlap(
  proposedName,
  proposedPurpose,
  existing,
  type,
  token,
) {
  const typeLabel =
    type === "value-stream" ? "Value Stream" : "Management Practice";
 
  const systemPrompt = `You are a MECE (Mutually Exclusive, Collectively Exhaustive) validation assistant for a company handbook. Your job is to determine if a proposed ${typeLabel} overlaps with any existing ${typeLabel}s.
 
Rules:
- MECE means each ${typeLabel} should have a distinct, non-overlapping scope
- Two ${typeLabel}s overlap if they could reasonably be merged or if work would be ambiguous about which one it belongs to
- Consider both explicit purpose statements and implied scope
- Be pragmatic — minor tangential connections are NOT overlaps
 
Respond in JSON format:
{
  "overlapRisk": "low" | "medium" | "high",
  "concerns": ["specific concern 1", "..."],
  "recommendation": "brief recommendation"
}`;
 
  const existingList = existing
    .map((e) => `- **${e.name}:** ${e.purpose}`)
    .join("\n");
 
  const userPrompt = `## Proposed ${typeLabel}
 
**Name:** ${proposedName}
**Purpose:** ${proposedPurpose}
 
## Existing ${typeLabel}s
 
${existingList || "(None exist yet)"}
 
Analyse whether the proposed ${typeLabel} overlaps with any existing ones. Consider scope boundaries, customer overlap, and potential ambiguity about where work belongs.`;
 
  try {
    const response = await fetch(
      "https://models.github.ai/inference/chat/completions",
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          model: "openai/gpt-4o",
          messages: [
            { role: "system", content: systemPrompt },
            { role: "user", content: userPrompt },
          ],
          temperature: 0.2,
          response_format: { type: "json_object" },
        }),
      },
    );
 
    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(
        `GitHub Models API error (${response.status}): ${errorText}`,
      );
    }
 
    const data = await response.json();
    const result = JSON.parse(data.choices[0].message.content);
 
    return {
      overlapRisk: result.overlapRisk || "unknown",
      concerns: result.concerns || [],
      recommendation: result.recommendation || "Unable to determine",
      comparedAgainst: existing.map((e) => e.name),
    };
  } catch (error) {
    // Graceful degradation — if AI check fails, return a warning instead of blocking
    return {
      overlapRisk: "unknown",
      concerns: [`AI MECE check failed: ${error.message}`],
      recommendation:
        "Manual MECE review recommended — the AI-powered check was unable to complete.",
      comparedAgainst: existing.map((e) => e.name),
    };
  }
}

Environment Variable: The workflow will pass GITHUB_TOKEN (automatically available in Actions) or a dedicated GITHUB_MODELS_TOKEN secret if needed.

Verification: Test with a known-overlapping purpose (e.g., propose “Design to Deploy” and compare against “Discovery to Deployment”) and verify it flags the overlap.


Step 5: Create CODEOWNERS Management Script

Goal: Manage the .github/CODEOWNERS file — bootstrap it from the deferred plan if it doesn’t exist, append entries for new VS/Practices, and verify team existence via the GitHub API.

File: scripts/update-codeowners.mjs

// scripts/update-codeowners.mjs
import { readFile, writeFile, access } from "node:fs/promises";
import { resolve } from "node:path";
 
const CODEOWNERS_PATH = resolve(process.cwd(), ".github", "CODEOWNERS");
 
/**
 * The initial CODEOWNERS content, bootstrapped from docs/CODEOWNERS_DEFERRED.md.
 * Used only if the file doesn't exist yet.
 */
const INITIAL_CODEOWNERS = `# CODEOWNERS — Calab.ai Handbook
# Auto-managed by scaffolding automation. Manual edits are preserved.
# See: docs/CODEOWNERS_DEFERRED.md for governance context.
 
# Company Governance - Executive Guild only
/content/00 Governance/** @calab-ai/guild-executive
 
# Value Streams - General ownership
/content/01 Value Streams/** @calab-ai/owners-value-stream
 
# Value Stream - Specific overrides
# (New entries are appended here by the scaffolding automation)
 
# Guilds
/content/02 Guilds/GL01 Executive Guild/** @calab-ai/guild-executive
/content/02 Guilds/GL02 Sales Guild/** @calab-ai/guild-sales
/content/02 Guilds/GL03 Delivery Guild/** @calab-ai/guild-delivery
/content/02 Guilds/GL04 Technology Guild/** @calab-ai/guild-technology
/content/02 Guilds/GL05 Administration Guild/** @calab-ai/guild-administration
 
# Practice-specific overrides
# (New entries are appended here by the scaffolding automation)
 
# Products
/content/03 Products/** @calab-ai/owners-product
 
# Archive - Executive Guild only
/content/99 Archive/** @calab-ai/guild-executive
`;
 
/**
 * Ensures the CODEOWNERS file exists. Creates it from the initial template if not.
 * @returns {Promise<boolean>} - true if file was created, false if it already existed
 */
export async function ensureCodeownersExists() {
  try {
    await access(CODEOWNERS_PATH);
    return false;
  } catch {
    await writeFile(CODEOWNERS_PATH, INITIAL_CODEOWNERS, "utf-8");
    return true;
  }
}
 
/**
 * Appends a CODEOWNERS entry in the appropriate section.
 * @param {string} contentPath - e.g., "/content/01 Value Streams/VS08 Plan to Innovate/**"
 * @param {string} team - e.g., "@calab-ai/plan-to-innovate-team"
 * @param {"value-stream" | "practice"} type
 */
export async function addCodeownersEntry(contentPath, team, type) {
  await ensureCodeownersExists();
  let content = await readFile(CODEOWNERS_PATH, "utf-8");
 
  const entry = `${contentPath} ${team}`;
 
  // Check if entry already exists
  if (content.includes(contentPath)) {
    return { added: false, reason: "Entry already exists" };
  }
 
  // Find the appropriate insertion point based on type
  const sectionMarker =
    type === "value-stream"
      ? "# Value Stream - Specific overrides"
      : "# Practice-specific overrides";
 
  const markerIndex = content.indexOf(sectionMarker);
  if (markerIndex !== -1) {
    // Find the end of the comment line after the marker
    const afterMarker = content.indexOf("\n", markerIndex);
    // Find the next non-comment, non-empty line or next section
    let insertionPoint = afterMarker + 1;
    const lines = content.slice(insertionPoint).split("\n");
    let offset = 0;
    for (const line of lines) {
      if (line.startsWith("#") && !line.startsWith("# (")) break;
      if (line.trim() === "") {
        offset += line.length + 1;
        continue;
      }
      if (line.startsWith("/content/")) {
        offset += line.length + 1;
        continue;
      }
      break;
    }
    insertionPoint += offset;
 
    content =
      content.slice(0, insertionPoint) +
      entry +
      "\n" +
      content.slice(insertionPoint);
  } else {
    // Fallback: append to end
    content += "\n" + entry + "\n";
  }
 
  await writeFile(CODEOWNERS_PATH, content, "utf-8");
  return { added: true };
}
 
/**
 * Verifies whether a GitHub team exists in the organisation.
 * @param {string} org - e.g., "calab-ai"
 * @param {string} teamSlug - e.g., "plan-to-innovate-team"
 * @param {string} token - GitHub token
 * @returns {Promise<{ exists: boolean, warning?: string }>}
 */
export async function verifyTeamExists(org, teamSlug, token) {
  try {
    const response = await fetch(
      `https://api.github.com/orgs/${org}/teams/${teamSlug}`,
      {
        headers: {
          Authorization: `Bearer ${token}`,
          Accept: "application/vnd.github+json",
          "X-GitHub-Api-Version": "2022-11-28",
        },
      },
    );
 
    if (response.status === 200) {
      return { exists: true };
    } else if (response.status === 404) {
      return {
        exists: false,
        warning: `GitHub Team @${org}/${teamSlug} does not exist yet. The CODEOWNERS entry has been added but will not be functional until the team is created. See docs/CODEOWNERS_DEFERRED.md for the team creation process.`,
      };
    } else {
      return {
        exists: false,
        warning: `Unable to verify team @${org}/${teamSlug} (API returned ${response.status}). Please verify manually.`,
      };
    }
  } catch (error) {
    return {
      exists: false,
      warning: `Unable to verify team @${org}/${teamSlug}: ${error.message}. Please verify manually.`,
    };
  }
}

Verification: Run locally to verify CODEOWNERS creation and entry append:

node -e "
  import { ensureCodeownersExists } from './scripts/update-codeowners.mjs';
  const created = await ensureCodeownersExists();
  console.log('Created:', created);
"

Step 6: Create GitHub Issue Form Templates

Goal: Structured YAML issue forms that collect all required inputs for VS/Practice creation.

6A. Value Stream Issue Form

File: .github/ISSUE_TEMPLATE/create-value-stream.yml

name: "Create Value Stream"
description: "Request creation of a new Value Stream with automated scaffolding"
title: "[VS] "
labels: ["scaffolding", "value-stream"]
body:
  - type: markdown
    attributes:
      value: |
        ## New Value Stream Request
 
        Fill in the fields below to create a new Value Stream. Once submitted, automation will:
        1. Validate naming, codes, and structure
        2. Run a MECE overlap analysis against existing Value Streams
        3. Scaffold the complete folder structure and files
        4. Create a Pull Request with all generated content for your review
 
        **All fields marked with * are required.**
 
  - type: input
    id: vs-name
    attributes:
      label: "Value Stream Name"
      description: "The descriptive name (without VS code prefix). Use Title Case."
      placeholder: "e.g., Plan to Innovate"
    validations:
      required: true
 
  - type: input
    id: vs-code
    attributes:
      label: "VS Code"
      description: "A unique code in the format VS## (e.g., VS08). Must not already exist."
      placeholder: "e.g., VS08"
    validations:
      required: true
 
  - type: textarea
    id: purpose
    attributes:
      label: "Purpose and Outcomes"
      description: "2-3 sentences describing the end-to-end value this stream delivers."
      placeholder: "This value stream represents..."
    validations:
      required: true
 
  - type: input
    id: primary-customer
    attributes:
      label: "Primary Customer"
      description: "Who receives the value from this stream?"
      placeholder: "e.g., External clients, Internal product teams"
    validations:
      required: true
 
  - type: textarea
    id: stakeholders
    attributes:
      label: "Key Stakeholders"
      description: "List stakeholders and their interests (one per line)."
      placeholder: |
        - Sales Team - Pipeline visibility
        - Finance - Revenue recognition
    validations:
      required: false
 
  - type: input
    id: start-trigger
    attributes:
      label: "Start Trigger"
      description: "What event or condition initiates this value stream?"
      placeholder: "e.g., New sales lead identified"
    validations:
      required: true
 
  - type: input
    id: end-trigger
    attributes:
      label: "End Trigger"
      description: "What outcome or deliverable completes this value stream?"
      placeholder: "e.g., Payment received and project closed"
    validations:
      required: true
 
  - type: input
    id: typical-duration
    attributes:
      label: "Typical Duration"
      description: "How long from start to end under normal conditions?"
      placeholder: "e.g., 2-6 months"
    validations:
      required: false
 
  - type: input
    id: vs-owner
    attributes:
      label: "Value Stream Owner"
      description: "Name or role title of the person who owns this value stream."
      placeholder: "e.g., Head of Innovation"
    validations:
      required: true
 
  - type: input
    id: team-slug
    attributes:
      label: "GitHub Team Slug"
      description: "The GitHub team slug for CODEOWNERS (without @calab-ai/ prefix)."
      placeholder: "e.g., plan-to-innovate-team"
    validations:
      required: true
 
  - type: textarea
    id: related-practices
    attributes:
      label: "Related Practices"
      description: "Comma-separated list of existing management practices this VS coordinates."
      placeholder: "e.g., Project Mgmt, Product Mgmt, Strategy Mgmt"
    validations:
      required: false
 
  - type: textarea
    id: additional-context
    attributes:
      label: "Additional Context"
      description: "Any extra information or context for reviewers."
    validations:
      required: false

6B. Management Practice Issue Form

File: .github/ISSUE_TEMPLATE/create-management-practice.yml

name: "Create Management Practice"
description: "Request creation of a new Management Practice with automated scaffolding"
title: "[Practice] "
labels: ["scaffolding", "management-practice"]
body:
  - type: markdown
    attributes:
      value: |
        ## New Management Practice Request
 
        Fill in the fields below to create a new Management Practice. Once submitted, automation will:
        1. Validate naming, Practice ID, and parent Guild
        2. Run a MECE overlap analysis against existing practices
        3. Scaffold the complete folder structure and files
        4. Create a Pull Request with all generated content for your review
 
        **All fields marked with * are required.**
 
  - type: input
    id: practice-name
    attributes:
      label: "Practice Name"
      description: "Must end with 'Management' (e.g., 'Change Management'). Use Title Case."
      placeholder: "e.g., Change Management"
    validations:
      required: true
 
  - type: input
    id: practice-id
    attributes:
      label: "Practice ID"
      description: "Unique ID in format GUILD-ABBR-## (e.g., DEL-CHG-01). Check existing IDs to avoid collisions."
      placeholder: "e.g., DEL-CHG-01"
    validations:
      required: true
 
  - type: dropdown
    id: parent-guild
    attributes:
      label: "Parent Guild"
      description: "Which Guild will own this practice?"
      options:
        - "GL01 Executive Guild"
        - "GL02 Sales Guild"
        - "GL03 Delivery Guild"
        - "GL04 Technology Guild"
        - "GL05 Administration Guild"
    validations:
      required: true
 
  - type: textarea
    id: purpose
    attributes:
      label: "Purpose and Objectives"
      description: "2-3 sentences describing what this practice aims to achieve and why it exists."
      placeholder: "This practice aims to..."
    validations:
      required: true
 
  - type: textarea
    id: in-scope
    attributes:
      label: "In Scope"
      description: "What is covered by this practice? One item per line."
      placeholder: |
        - Change request assessment and classification
        - Impact analysis and approval workflows
    validations:
      required: true
 
  - type: textarea
    id: out-of-scope
    attributes:
      label: "Out of Scope"
      description: "What is explicitly NOT covered? One item per line."
      placeholder: |
        - Incident management (covered by separate practice)
        - Infrastructure provisioning
    validations:
      required: true
 
  - type: input
    id: owner-role
    attributes:
      label: "Practice Owner Role"
      description: "The role title responsible for this practice."
      placeholder: "e.g., Change Management Lead"
    validations:
      required: true
 
  - type: input
    id: team-slug
    attributes:
      label: "GitHub Team Slug (optional)"
      description: "Practice-specific team for CODEOWNERS override (without @calab-ai/ prefix). Leave blank to use the Guild team."
      placeholder: "e.g., change-mgmt-team"
    validations:
      required: false
 
  - type: textarea
    id: dependencies
    attributes:
      label: "Dependencies (Practices We Depend On)"
      description: "Comma-separated list of practices this depends on."
      placeholder: "e.g., Project Mgmt, Solution Eng"
    validations:
      required: false
 
  - type: textarea
    id: dependents
    attributes:
      label: "Dependents (Practices That Depend On Us)"
      description: "Comma-separated list of practices that depend on this."
      placeholder: "e.g., Software Eng"
    validations:
      required: false
 
  - type: textarea
    id: related-vs
    attributes:
      label: "Related Value Streams"
      description: "Which Value Streams does this practice participate in?"
      placeholder: "e.g., VS03 Request to Release, VS04 Incident to Resolution"
    validations:
      required: false
 
  - type: textarea
    id: additional-context
    attributes:
      label: "Additional Context"
      description: "Any extra information or context for reviewers."
    validations:
      required: false

Verification: Navigate to the repository’s Issues tab → “New Issue” and verify both forms render correctly with proper field types and validation.


Step 7: Create GitHub Actions Workflows

Goal: Automated workflows that trigger on issue creation, run validations, scaffold files, and create PRs.

7A. Scaffold Value Stream Workflow

File: .github/workflows/scaffold-value-stream.yml

name: "Scaffold Value Stream"
 
on:
  issues:
    types: [opened]
 
permissions:
  contents: write
  issues: write
  pull-requests: write
  models: read
 
jobs:
  scaffold:
    # Only run for issues with the 'value-stream' label
    if: contains(github.event.issue.labels.*.name, 'value-stream')
    runs-on: ubuntu-latest
 
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "22"
 
      - name: Install script dependencies
        run: npm install gray-matter
 
      - name: Parse issue and run validations
        id: validate
        uses: actions/github-script@v7
        with:
          script: |
            const { parseIssueBody, parseList } = await import(
              `${process.cwd()}/scripts/parse-issue.mjs`
            );
            const { validateValueStream } = await import(
              `${process.cwd()}/scripts/validate-structure.mjs`
            );
 
            const body = context.payload.issue.body;
            const fields = parseIssueBody(body);
 
            // Map form fields to inputs
            const inputs = {
              name: fields["Value Stream Name"]?.trim(),
              code: fields["VS Code"]?.trim().toUpperCase(),
              purpose: fields["Purpose and Outcomes"]?.trim(),
              primaryCustomer: fields["Primary Customer"]?.trim(),
              stakeholders: fields["Key Stakeholders"]?.trim() || "",
              startTrigger: fields["Start Trigger"]?.trim(),
              endTrigger: fields["End Trigger"]?.trim(),
              typicalDuration: fields["Typical Duration"]?.trim() || "",
              owner: fields["Value Stream Owner"]?.trim(),
              ownerSlug: (fields["Value Stream Owner"] || "")
                .trim()
                .toLowerCase()
                .replace(/\s+/g, "-"),
              teamSlug: fields["GitHub Team Slug"]?.trim(),
              relatedPractices: parseList(fields["Related Practices"] || ""),
              additionalContext: fields["Additional Context"]?.trim() || "",
              author: context.payload.issue.user.login,
            };
 
            // Validate
            const validation = await validateValueStream({
              code: inputs.code,
              name: inputs.name,
              relatedPractices: inputs.relatedPractices,
            });
 
            core.setOutput("inputs", JSON.stringify(inputs));
            core.setOutput("validation", JSON.stringify(validation));
            core.setOutput("passed", validation.passed.toString());
 
      - name: Run MECE analysis
        id: mece
        if: steps.validate.outputs.passed == 'true'
        uses: actions/github-script@v7
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          script: |
            const { collectExistingVSPurposes, checkMECEOverlap } = await import(
              `${process.cwd()}/scripts/mece-check.mjs`
            );
 
            const inputs = JSON.parse('${{ steps.validate.outputs.inputs }}');
            const existing = await collectExistingVSPurposes();
 
            const meceResult = await checkMECEOverlap(
              inputs.name,
              inputs.purpose,
              existing,
              "value-stream",
              process.env.GITHUB_TOKEN,
            );
 
            core.setOutput("mece", JSON.stringify(meceResult));
 
      - name: Scaffold files
        id: scaffold
        if: steps.validate.outputs.passed == 'true'
        uses: actions/github-script@v7
        with:
          script: |
            const { scaffoldValueStream } = await import(
              `${process.cwd()}/scripts/scaffold-files.mjs`
            );
 
            const inputs = JSON.parse('${{ steps.validate.outputs.inputs }}');
            const files = await scaffoldValueStream(inputs);
 
            core.setOutput("files", JSON.stringify(files));
 
      - name: Update CODEOWNERS
        id: codeowners
        if: steps.validate.outputs.passed == 'true'
        uses: actions/github-script@v7
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          script: |
            const { addCodeownersEntry, verifyTeamExists } = await import(
              `${process.cwd()}/scripts/update-codeowners.mjs`
            );
 
            const inputs = JSON.parse('${{ steps.validate.outputs.inputs }}');
            const contentPath = `/content/01 Value Streams/${inputs.code} ${inputs.name}/**`;
            const team = `@calab-ai/${inputs.teamSlug}`;
 
            const entryResult = await addCodeownersEntry(
              contentPath,
              team,
              "value-stream",
            );
            const teamResult = await verifyTeamExists(
              "calab-ai",
              inputs.teamSlug,
              process.env.GITHUB_TOKEN,
            );
 
            core.setOutput(
              "codeowners",
              JSON.stringify({ ...entryResult, ...teamResult }),
            );
 
      - name: Create branch and PR
        if: steps.validate.outputs.passed == 'true'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          INPUTS='${{ steps.validate.outputs.inputs }}'
          VALIDATION='${{ steps.validate.outputs.validation }}'
          MECE='${{ steps.mece.outputs.mece }}'
          FILES='${{ steps.scaffold.outputs.files }}'
          CODEOWNERS='${{ steps.codeowners.outputs.codeowners }}'
 
          CODE=$(echo "$INPUTS" | jq -r '.code')
          NAME=$(echo "$INPUTS" | jq -r '.name')
          SLUG=$(echo "$NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
          BRANCH="scaffolding/${CODE,,}-${SLUG}"
          ISSUE_NUM=${{ github.event.issue.number }}
 
          # Configure git
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
 
          # Create branch and commit
          git checkout -b "$BRANCH"
          git add -A
          git commit -m "Scaffold: ${CODE} ${NAME} (closes #${ISSUE_NUM})"
          git push -u origin "$BRANCH"
 
          # Build PR body
          OVERLAP_RISK=$(echo "$MECE" | jq -r '.overlapRisk // "unknown"')
          RECOMMENDATION=$(echo "$MECE" | jq -r '.recommendation // "N/A"')
          CONCERNS=$(echo "$MECE" | jq -r '.concerns // [] | if length > 0 then map("- " + .) | join("\n") else "None identified" end')
          COMPARED=$(echo "$MECE" | jq -r '.comparedAgainst // [] | if length > 0 then map("- " + .) | join("\n") else "None" end')
          TEAM_WARNING=$(echo "$CODEOWNERS" | jq -r '.warning // "Team verified successfully"')
          FILE_LIST=$(echo "$FILES" | jq -r '.[] | "- `" + . + "`"' | head -20)
 
          # Format validation table
          VALIDATION_TABLE=$(echo "$VALIDATION" | jq -r '.results[] | "| " + .check + " | " + (if .passed then "✅" else "❌" end) + " | " + .detail + " |"')
 
          # Create PR
          gh pr create \
            --base main \
            --head "$BRANCH" \
            --title "[Scaffolding] ${CODE} ${NAME} — New Value Stream" \
            --body "$(cat <<EOF
          ## Scaffolding Report
 
          **Type:** Value Stream
          **Source Issue:** #${ISSUE_NUM}
          **Generated:** $(date -u +"%Y-%m-%d %H:%M UTC")
 
          ### Validation Results
 
          | Check | Result | Details |
          |-------|--------|---------|
          ${VALIDATION_TABLE}
 
          ### MECE Analysis
 
          **Overlap Risk:** ${OVERLAP_RISK}
          **Recommendation:** ${RECOMMENDATION}
 
          ${CONCERNS}
 
          <details><summary>Compared Against</summary>
 
          ${COMPARED}
 
          </details>
 
          ### CODEOWNERS
 
          ${TEAM_WARNING}
 
          ### Generated Files
 
          ${FILE_LIST}
 
          ### Next Steps
 
          - [ ] Review generated content for accuracy
          - [ ] Edit frontmatter fields (especially \`owner\` slug)
          - [ ] Verify related practice cross-references
          - [ ] Confirm CODEOWNERS entry is correct
          - [ ] Update \`README.md\` with any additional detail
 
          ---
          Closes #${ISSUE_NUM}
          EOF
          )"
 
      - name: Comment on issue (failure)
        if: steps.validate.outputs.passed == 'false'
        uses: actions/github-script@v7
        with:
          script: |
            const validation = JSON.parse('${{ steps.validate.outputs.validation }}');
            const table = validation.results
              .map(
                (r) =>
                  `| ${r.check} | ${r.passed ? "✅" : "❌"} | ${r.detail} |`,
              )
              .join("\n");
 
            const body = `## ❌ Scaffolding Validation Failed
 
            The following checks did not pass. Please update the issue with corrected values.
 
            | Check | Result | Details |
            |-------|--------|---------|
            ${table}
 
            Please close this issue and create a new one with corrected values, or edit this issue to fix the errors and re-open it.`;
 
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body,
            });

7B. Scaffold Management Practice Workflow

File: .github/workflows/scaffold-management-practice.yml

This workflow follows the identical pattern as 7A but uses practice-specific parsing, validation, scaffolding, and CODEOWNERS logic. The key differences:

  • Parses practice-specific fields (Practice ID, Parent Guild, In/Out Scope, Dependencies, Dependents)
  • Uses validatePractice() instead of validateValueStream()
  • Uses collectExistingPracticePurposes() for MECE check (with same-guild primary check)
  • Uses scaffoldPractice() instead of scaffoldValueStream()
  • CODEOWNERS entry uses practice-specific path and optional team slug
name: "Scaffold Management Practice"
 
on:
  issues:
    types: [opened]
 
permissions:
  contents: write
  issues: write
  pull-requests: write
  models: read
 
jobs:
  scaffold:
    if: contains(github.event.issue.labels.*.name, 'management-practice')
    runs-on: ubuntu-latest
 
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "22"
 
      - name: Install script dependencies
        run: npm install gray-matter
 
      - name: Parse issue and run validations
        id: validate
        uses: actions/github-script@v7
        with:
          script: |
            const { parseIssueBody, parseList, parseGuildSelection } = await import(
              `${process.cwd()}/scripts/parse-issue.mjs`
            );
            const { validatePractice } = await import(
              `${process.cwd()}/scripts/validate-structure.mjs`
            );
 
            const body = context.payload.issue.body;
            const fields = parseIssueBody(body);
 
            const guild = parseGuildSelection(fields["Parent Guild"]);
 
            const inputs = {
              name: fields["Practice Name"]?.trim(),
              id: fields["Practice ID"]?.trim().toUpperCase(),
              guildFolder: guild.folderName,
              guildCode: guild.code,
              purpose: fields["Purpose and Objectives"]?.trim(),
              inScope: fields["In Scope"]?.trim() || "",
              outOfScope: fields["Out of Scope"]?.trim() || "",
              ownerRole: fields["Practice Owner Role"]?.trim(),
              ownerSlug: (fields["Practice Owner Role"] || "")
                .trim()
                .toLowerCase()
                .replace(/\s+/g, "-"),
              teamSlug: fields["GitHub Team Slug (optional)"]?.trim() || "",
              dependencies: parseList(
                fields["Dependencies (Practices We Depend On)"] || "",
              ),
              dependents: parseList(
                fields["Dependents (Practices That Depend On Us)"] || "",
              ),
              relatedVS: parseList(fields["Related Value Streams"] || ""),
              additionalContext: fields["Additional Context"]?.trim() || "",
              author: context.payload.issue.user.login,
            };
 
            const validation = await validatePractice({
              id: inputs.id,
              name: inputs.name,
              guildFolder: inputs.guildFolder,
              dependencies: inputs.dependencies,
              dependents: inputs.dependents,
            });
 
            core.setOutput("inputs", JSON.stringify(inputs));
            core.setOutput("validation", JSON.stringify(validation));
            core.setOutput("passed", validation.passed.toString());
 
      - name: Run MECE analysis
        id: mece
        if: steps.validate.outputs.passed == 'true'
        uses: actions/github-script@v7
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          script: |
            const {
              collectExistingPracticePurposes,
              checkMECEOverlap,
            } = await import(`${process.cwd()}/scripts/mece-check.mjs`);
 
            const inputs = JSON.parse('${{ steps.validate.outputs.inputs }}');
 
            // Primary check: same guild
            const sameGuild = await collectExistingPracticePurposes(
              inputs.guildFolder,
            );
            // Secondary check: all guilds
            const allPractices = await collectExistingPracticePurposes();
 
            const meceResult = await checkMECEOverlap(
              inputs.name,
              inputs.purpose,
              allPractices,
              "practice",
              process.env.GITHUB_TOKEN,
            );
 
            // Add same-guild specific note
            const sameGuildNames = sameGuild.map((p) => p.name);
            meceResult.sameGuildPractices = sameGuildNames;
 
            core.setOutput("mece", JSON.stringify(meceResult));
 
      - name: Scaffold files
        id: scaffold
        if: steps.validate.outputs.passed == 'true'
        uses: actions/github-script@v7
        with:
          script: |
            const { scaffoldPractice } = await import(
              `${process.cwd()}/scripts/scaffold-files.mjs`
            );
 
            const inputs = JSON.parse('${{ steps.validate.outputs.inputs }}');
            const files = await scaffoldPractice(inputs);
 
            core.setOutput("files", JSON.stringify(files));
 
      - name: Update CODEOWNERS
        id: codeowners
        if: steps.validate.outputs.passed == 'true' && contains(steps.validate.outputs.inputs, '"teamSlug":')
        uses: actions/github-script@v7
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          script: |
            const { addCodeownersEntry, verifyTeamExists } = await import(
              `${process.cwd()}/scripts/update-codeowners.mjs`
            );
 
            const inputs = JSON.parse('${{ steps.validate.outputs.inputs }}');
 
            // Only add practice-specific CODEOWNERS if a team slug was provided
            if (!inputs.teamSlug) {
              core.setOutput(
                "codeowners",
                JSON.stringify({
                  added: false,
                  reason: "No practice-specific team — using guild-level ownership",
                  exists: true,
                }),
              );
              return;
            }
 
            const contentPath = `/content/02 Guilds/${inputs.guildFolder}/Practices/${inputs.name}/**`;
            const team = `@calab-ai/${inputs.teamSlug}`;
 
            const entryResult = await addCodeownersEntry(
              contentPath,
              team,
              "practice",
            );
            const teamResult = await verifyTeamExists(
              "calab-ai",
              inputs.teamSlug,
              process.env.GITHUB_TOKEN,
            );
 
            core.setOutput(
              "codeowners",
              JSON.stringify({ ...entryResult, ...teamResult }),
            );
 
      - name: Create branch and PR
        if: steps.validate.outputs.passed == 'true'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          INPUTS='${{ steps.validate.outputs.inputs }}'
          VALIDATION='${{ steps.validate.outputs.validation }}'
          MECE='${{ steps.mece.outputs.mece }}'
          FILES='${{ steps.scaffold.outputs.files }}'
          CODEOWNERS='${{ steps.codeowners.outputs.codeowners }}'
 
          NAME=$(echo "$INPUTS" | jq -r '.name')
          GUILD=$(echo "$INPUTS" | jq -r '.guildCode')
          SLUG=$(echo "$NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
          BRANCH="scaffolding/practice-${SLUG}"
          ISSUE_NUM=${{ github.event.issue.number }}
 
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
 
          git checkout -b "$BRANCH"
          git add -A
          git commit -m "Scaffold: ${NAME} in ${GUILD} (closes #${ISSUE_NUM})"
          git push -u origin "$BRANCH"
 
          OVERLAP_RISK=$(echo "$MECE" | jq -r '.overlapRisk // "unknown"')
          RECOMMENDATION=$(echo "$MECE" | jq -r '.recommendation // "N/A"')
          CONCERNS=$(echo "$MECE" | jq -r '.concerns // [] | if length > 0 then map("- " + .) | join("\n") else "None identified" end')
          COMPARED=$(echo "$MECE" | jq -r '.comparedAgainst // [] | if length > 0 then map("- " + .) | join("\n") else "None" end')
          SAME_GUILD=$(echo "$MECE" | jq -r '.sameGuildPractices // [] | if length > 0 then map("- " + .) | join("\n") else "None" end')
          TEAM_WARNING=$(echo "$CODEOWNERS" | jq -r '.warning // .reason // "OK"')
          FILE_LIST=$(echo "$FILES" | jq -r '.[] | "- `" + . + "`"' | head -25)
 
          VALIDATION_TABLE=$(echo "$VALIDATION" | jq -r '.results[] | "| " + .check + " | " + (if .passed then "✅" else "❌" end) + " | " + .detail + " |"')
 
          gh pr create \
            --base main \
            --head "$BRANCH" \
            --title "[Scaffolding] ${NAME} — New Management Practice" \
            --body "$(cat <<EOF
          ## Scaffolding Report
 
          **Type:** Management Practice
          **Parent Guild:** $(echo "$INPUTS" | jq -r '.guildFolder')
          **Source Issue:** #${ISSUE_NUM}
          **Generated:** $(date -u +"%Y-%m-%d %H:%M UTC")
 
          ### Validation Results
 
          | Check | Result | Details |
          |-------|--------|---------|
          ${VALIDATION_TABLE}
 
          ### MECE Analysis
 
          **Overlap Risk:** ${OVERLAP_RISK}
          **Recommendation:** ${RECOMMENDATION}
 
          ${CONCERNS}
 
          <details><summary>Practices in Same Guild</summary>
 
          ${SAME_GUILD}
 
          </details>
 
          <details><summary>All Practices Compared Against</summary>
 
          ${COMPARED}
 
          </details>
 
          ### CODEOWNERS
 
          ${TEAM_WARNING}
 
          ### Generated Files
 
          ${FILE_LIST}
 
          ### Next Steps
 
          - [ ] Review generated content for accuracy
          - [ ] Edit frontmatter fields (especially \`owner\` slug)
          - [ ] Verify dependency/dependent cross-references
          - [ ] Confirm CODEOWNERS entry is correct (if applicable)
          - [ ] Update \`README.md\` with additional detail
 
          ---
          Closes #${ISSUE_NUM}
          EOF
          )"
 
      - name: Comment on issue (failure)
        if: steps.validate.outputs.passed == 'false'
        uses: actions/github-script@v7
        with:
          script: |
            const validation = JSON.parse('${{ steps.validate.outputs.validation }}');
            const table = validation.results
              .map(
                (r) =>
                  `| ${r.check} | ${r.passed ? "✅" : "❌"} | ${r.detail} |`,
              )
              .join("\n");
 
            const body = `## ❌ Scaffolding Validation Failed
 
            The following checks did not pass. Please update the issue with corrected values.
 
            | Check | Result | Details |
            |-------|--------|---------|
            ${table}
 
            Please close this issue and create a new one with corrected values, or edit this issue to fix the errors and re-open it.`;
 
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body,
            });

Verification: Create a test issue with the value-stream label and verify the workflow triggers, validates, scaffolds, and creates a PR. Check Actions tab for logs.


Step 8: Create Content Validation Workflow

Goal: A CI gate that validates content structure on all PRs touching content/**.

File: .github/workflows/validate-content.yml

name: "Validate Content"
 
on:
  pull_request:
    paths:
      - "content/**"
      - ".github/CODEOWNERS"
 
permissions:
  contents: read
  pull-requests: write
 
jobs:
  validate:
    runs-on: ubuntu-latest
 
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "22"
 
      - name: Install dependencies
        run: npm install gray-matter
 
      - name: Validate frontmatter
        id: frontmatter
        uses: actions/github-script@v7
        with:
          script: |
            const { validateAllContent } = await import(
              `${process.cwd()}/scripts/validate-frontmatter.mjs`
            );
 
            const results = await validateAllContent("content");
            const errors = results.filter((r) => !r.valid);
 
            if (errors.length > 0) {
              const errorList = errors
                .map((e) => `- \`${e.file}\`: ${e.errors.join(", ")}`)
                .join("\n");
              core.setOutput("errors", errorList);
              core.setOutput("passed", "false");
            } else {
              core.setOutput("passed", "true");
              core.setOutput("errors", "");
            }
 
      - name: Check for duplicate codes
        id: duplicates
        uses: actions/github-script@v7
        with:
          script: |
            const {
              getExistingVSCodes,
              getExistingPracticeIds,
            } = await import(`${process.cwd()}/scripts/validate-structure.mjs`);
 
            const vsCodes = await getExistingVSCodes();
            const practiceIds = await getExistingPracticeIds();
 
            const duplicateVS = vsCodes.filter(
              (c, i) => vsCodes.indexOf(c) !== i,
            );
            const duplicatePractice = practiceIds.filter(
              (p, i) => practiceIds.findIndex((q) => q.id === p.id) !== i,
            );
 
            const errors = [];
            if (duplicateVS.length > 0) {
              errors.push(`Duplicate VS codes: ${duplicateVS.join(", ")}`);
            }
            if (duplicatePractice.length > 0) {
              errors.push(
                `Duplicate Practice IDs: ${duplicatePractice.map((p) => p.id).join(", ")}`,
              );
            }
 
            core.setOutput("passed", errors.length === 0 ? "true" : "false");
            core.setOutput("errors", errors.join("\n"));
 
      - name: Post validation results
        if: always()
        uses: actions/github-script@v7
        with:
          script: |
            const fmPassed = "${{ steps.frontmatter.outputs.passed }}" === "true";
            const fmErrors = `${{ steps.frontmatter.outputs.errors }}`;
            const dupPassed = "${{ steps.duplicates.outputs.passed }}" === "true";
            const dupErrors = `${{ steps.duplicates.outputs.errors }}`;
 
            if (fmPassed && dupPassed) return; // All good, no comment needed
 
            const body = `## Content Validation Results
 
            ${!fmPassed ? `### ❌ Frontmatter Issues\n\n${fmErrors}\n` : "### ✅ Frontmatter OK\n"}
            ${!dupPassed ? `### ❌ Duplicate Codes\n\n${dupErrors}\n` : "### ✅ No Duplicate Codes\n"}`;
 
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.payload.pull_request.number,
              body,
            });
 
            if (!fmPassed || !dupPassed) {
              core.setFailed("Content validation failed");
            }

Supporting Script File: scripts/validate-frontmatter.mjs

// scripts/validate-frontmatter.mjs
import { readdir, readFile } from "node:fs/promises";
import { join, resolve, extname } from "node:path";
import matter from "gray-matter";
 
const CONTENT_DIR = resolve(process.cwd(), "content");
 
// Required frontmatter fields for all published content
const REQUIRED_FIELDS = ["page-status", "created", "owner", "type"];
 
// Additional required fields by type
const TYPE_REQUIRED_FIELDS = {
  "value-stream-overview": ["vs-code"],
  "practice-overview": ["practice-id", "guild"],
  "role-criteria": ["guild", "role", "level", "criteria-type"],
};
 
// Directories to skip entirely
const SKIP_DIRS = [".obsidian", "metadata", "99 Archive"];
 
/**
 * Validates frontmatter for a single markdown file.
 * @param {string} filePath - Absolute path to the file
 * @returns {{ valid: boolean, errors: string[], file: string }}
 */
export async function validateFrontmatter(filePath) {
  const errors = [];
  const relativePath = filePath.replace(CONTENT_DIR + "/", "");
 
  try {
    const content = await readFile(filePath, "utf-8");
    const { data } = matter(content);
 
    // Check if file has any frontmatter
    if (Object.keys(data).length === 0) {
      errors.push("Missing frontmatter");
      return { valid: false, errors, file: relativePath };
    }
 
    // Check required fields
    for (const field of REQUIRED_FIELDS) {
      if (
        data[field] === undefined ||
        data[field] === null ||
        data[field] === ""
      ) {
        errors.push(`Missing required field: ${field}`);
      }
    }
 
    // Check type-specific fields
    const type = data.type;
    if (type && TYPE_REQUIRED_FIELDS[type]) {
      for (const field of TYPE_REQUIRED_FIELDS[type]) {
        if (
          data[field] === undefined ||
          data[field] === null ||
          data[field] === ""
        ) {
          errors.push(`Missing required field for type "${type}": ${field}`);
        }
      }
    }
 
    // Validate page-status values
    const validStatuses = ["draft", "review", "active", "deprecated"];
    if (data["page-status"]) {
      const status = Array.isArray(data["page-status"])
        ? data["page-status"][0]
        : data["page-status"];
      if (!validStatuses.includes(status)) {
        errors.push(
          `Invalid page-status "${status}". Must be one of: ${validStatuses.join(", ")}`,
        );
      }
    }
 
    // Validate created date format
    if (data.created && !/^\d{4}-\d{2}-\d{2}$/.test(String(data.created))) {
      errors.push(
        `Invalid created date format "${data.created}". Must be YYYY-MM-DD`,
      );
    }
  } catch (error) {
    errors.push(`Error reading file: ${error.message}`);
  }
 
  return { valid: errors.length === 0, errors, file: relativePath };
}
 
/**
 * Recursively finds all markdown files in a directory, skipping excluded dirs.
 */
async function findMarkdownFiles(dir) {
  const files = [];
  const entries = await readdir(dir, { withFileTypes: true });
 
  for (const entry of entries) {
    if (
      SKIP_DIRS.some(
        (skip) => entry.name === skip || entry.name.startsWith("_"),
      )
    ) {
      continue;
    }
 
    const fullPath = join(dir, entry.name);
    if (entry.isDirectory()) {
      files.push(...(await findMarkdownFiles(fullPath)));
    } else if (extname(entry.name) === ".md") {
      files.push(fullPath);
    }
  }
 
  return files;
}
 
/**
 * Validates frontmatter for all markdown files in the content directory.
 * @param {string} [contentDirName="content"] - Content directory name relative to cwd
 * @returns {Promise<Array<{ valid: boolean, errors: string[], file: string }>>}
 */
export async function validateAllContent(contentDirName = "content") {
  const contentDir = resolve(process.cwd(), contentDirName);
  const files = await findMarkdownFiles(contentDir);
  const results = [];
 
  for (const file of files) {
    const result = await validateFrontmatter(file);
    results.push(result);
  }
 
  return results;
}

Verification: Run locally:

node -e "
  import { validateAllContent } from './scripts/validate-frontmatter.mjs';
  const results = await validateAllContent('content');
  const errors = results.filter(r => !r.valid);
  console.log('Files with issues:', errors.length);
  errors.forEach(e => console.log(e.file, e.errors));
"

Step 9: Create PR Template

Goal: A general-purpose PR template for all PRs, with scaffolding-specific sections.

File: .github/pull_request_template.md

## Description
 
<!-- What does this PR do? Why is it needed? -->
 
## Type of Change
 
- [ ] Content (documentation, handbook content)
- [ ] Scaffolding (new Value Stream or Management Practice)
- [ ] Tooling (CI/CD, scripts, configuration)
- [ ] Bug fix
 
## Scaffolding Checklist (if applicable)
 
- [ ] Folder structure follows Knowledge Standards
- [ ] All frontmatter fields populated
- [ ] MECE validation passed (no overlap concerns)
- [ ] CODEOWNERS entry added (if applicable)
- [ ] Related cross-references are accurate

Verification: Create a PR manually and verify the template pre-populates the PR body.


Step 10: Template Cleanup and Documentation Updates

Goal: Remove superseded Obsidian Templater templates and update all documentation references to maintain MECE integrity (no duplication, no stale references).

10A. Delete superseded templates

rm "content/metadata/templater/TMP01 Value Stream Overview.md"
rm "content/metadata/templater/TMP03 Practice Overview.md"

10B. Update content/00 Governance/01 How to Contribute.md

Changes:

  1. Replace the template reference at line 36 (content/_templater/) with instructions for the new scaffolding workflow
  2. Distinguish between VS/Practice creation (use Issue Form) and content within practices (manual)
  3. Fix the stale _templater path reference

Updated section (replace lines 26-56):

### 1. Identify Where Your Content Belongs
 
Use the content classification decision tree in [[Content Types|Content Types]] to determine:
 
- What type of content you're creating (Policy, Process, Procedure, Guide, Template, or Decision)
- Which Practice owns this type of content
- Where the file should be placed in the folder structure
 
### 2. Create Your Content
 
#### Creating a New Value Stream or Management Practice
 
New Value Streams and Management Practices are created through automated scaffolding:
 
1. Go to the [repository Issues tab](https://github.com/calab-ai/calab-handbook/issues/new/choose)
2. Select **"Create Value Stream"** or **"Create Management Practice"**
3. Fill in all required fields in the issue form
4. Submit the issue — automation will validate inputs, run MECE analysis, and create a Pull Request with the complete folder structure and populated files
5. Review the generated PR and make any needed edits
 
#### Creating Content Within an Existing Practice
 
For policies, processes, procedures, guides, templates, and decisions within an existing practice:
 
1. **Use the appropriate template** from `content/metadata/templater/` (available for decisions, Guilds, and Products)
2. **Follow naming conventions:**
   - Use descriptive names: `Incident Response Process.md`
   - Include version numbers if needed: `Security Baseline Policy v2.0.md`
3. **Add proper frontmatter:**
   ```yaml
   ---
   page-status: draft
   created: YYYY-MM-DD
   owner: [guild-slug]
   type: [policy|process|procedure|guide|template|decision]
   ---
   ```

3. Submit for Review

  1. Create a feature branch: git checkout -b feat/[description]
  2. Add your changes: git add .
  3. Commit with clear message: git commit -m "Add: [description]"
  4. Push to remote: git push -u origin feat/[description]
  5. Create a Pull Request

4. Review and Approval

  • Content validation runs automatically on all PRs touching content/
  • GitHub CODEOWNERS automatically assigns reviewers based on the file path
  • Address any feedback from reviewers
  • Once approved, your PR will be merged

#### 10C. Update `content/00 Governance/04 Decision Records.md`

**Change line 62:** Replace `content/_templater/TMP01 Create ADR.md` with `content/metadata/templater/TMP00 Create Decision.md` (pre-existing bug fix).

#### 10D. Update `content/02 Guilds/GL04 Technology Guild/Practices/Solution Eng/README.md`

**Change line 66:** Replace `ADR Template (TMP01)` with `Decision Template (TMP00)` (pre-existing bug fix).

#### 10E. Update `docs/REPOSITORY_STRUCTURE.md`

Add the new `scripts/` directory to the structure table:

```markdown
| `scripts/`             | Automation scripts (validation, scaffolding)   | N/A (not published)            |

Update the templater row:

| `content/metadata/templater/` | Obsidian note templates (Decision, Guild, Product) | No (ignored) |

10F. Update docs/CODEOWNERS_DEFERRED.md

Add a note at the top of the Status section:

## Status
 
**PARTIALLY ADDRESSED** — The scaffolding automation (Plan 08) now manages CODEOWNERS entries automatically when new Value Streams or Management Practices are created. The full CODEOWNERS implementation (including GitHub Teams creation) remains deferred.
 
See: `content/metadata/plans/08 Automated Content Scaffolding/README.md`

Verification: Search for all remaining references to TMP01, TMP03, and _templater across the repository. The only surviving references should be in historical content/metadata/plans/ files (which are immutable records of past plans) and this plan itself.

grep -r "TMP01\|TMP03\|_templater" content/ --include="*.md" -l
# Expected: No results (all references cleaned up from content/)

Step 11: Create Decision for This Automation

Goal: Document the architectural decision to adopt automated content scaffolding.

File: content/metadata/decisions/06 Automated Content Scaffolding.md

# Decision 06: Automated Content Scaffolding via GitHub Issue Forms
 
## Status
 
Accepted
 
## Context
 
Creating new Value Streams and Management Practices requires:
 
- Creating a specific folder structure with 6-10 files
- Populating frontmatter with correct metadata fields
- Following naming conventions (Title Case, "Management" suffix, VS code format)
- Ensuring MECE compliance (no overlap with existing VS/Practices)
- Updating CODEOWNERS entries
- Cross-referencing related practices
 
This process was entirely manual, relying on Obsidian Templater templates (TMP01, TMP03) and human diligence. This was error-prone (inconsistent frontmatter, missing folders, naming violations) and created friction for contributors.
 
## Decision
 
Implement a GitHub-native automation system:
 
1. **GitHub Issue Forms** as the structured input mechanism
2. **GitHub Actions workflows** triggered on issue creation to validate, scaffold, and create PRs
3. **Node.js ESM scripts** for validation, MECE checking, file generation, and CODEOWNERS management
4. **GitHub Models API** for AI-powered semantic overlap detection
5. **Content validation CI workflow** as a quality gate on all content PRs
 
The scaffolding scripts own the template logic (inline template strings), superseding the Obsidian Templater files TMP01 and TMP03.
 
## Consequences
 
### Positive
 
- Zero manual scaffolding — complete folder structure generated from structured inputs
- Governance enforcement via deterministic validation
- MECE integrity via AI-assisted semantic analysis
- Audit trail from issue → PR → merge
- CODEOWNERS proactively managed
 
### Negative
 
- Templates for VS/Practice creation are now in JavaScript, not easily editable Obsidian markdown
- Requires GitHub Actions minutes (minimal — runs only on issue creation)
- AI MECE check adds dependency on GitHub Models API (graceful degradation if unavailable)
 
### Neutral
 
- Obsidian Templater templates TMP00 (Decision), TMP02 (Guild), TMP05 (Product) remain unchanged
- Obsidian Templater plugin continues to work for remaining template types
- Historical plan documents retain references to TMP01/TMP03 as immutable records
 
## Alternatives Considered
 
### 1. Enhanced Obsidian Templater Scripts
 
Use Templater's JavaScript execution capabilities to add validation and folder creation.
 
- **Pros:** Stays within Obsidian ecosystem; familiar to content authors
- **Cons:** No CI enforcement; no MECE checking; no audit trail; only works locally; no CODEOWNERS management
- **Why not chosen:** Doesn't address governance requirements
 
### 2. CLI Script (npm run create:vs)
 
Local Node.js script with interactive prompts.
 
- **Pros:** Fast local feedback; no GitHub dependency
- **Cons:** No audit trail; no MECE checking; requires local Node.js setup; bypasses governance
- **Why not chosen:** Doesn't create the issue → PR → review governance chain
 
### 3. GitHub Actions with Manual Trigger (workflow_dispatch)
 
Workflow triggered manually after issue approval rather than on issue creation.
 
- **Pros:** Adds an explicit approval step before scaffolding
- **Cons:** Extra manual step; the PR itself already serves as the review/approval gate
- **Why not chosen:** The PR review process provides sufficient governance control
 
## Related Decisions
 
- Plan 01: Governance Model & Content Structure (established the folder patterns)
- CODEOWNERS Deferred (established team structure expectations)
- Plan 06: Site UX Improvements (established page-list auto-population)
 
---
 
**Date:** 2026-02-17
**Decided By:** Leadership Team
**Approved By:** [Pending]

Verification: Review decision for completeness and accuracy against the decision template structure in content/00 Governance/04 Decision Records.md.


4. Decision Points & Options

Decision 1: GitHub Models API Availability

The GitHub Models API is used for MECE checking. If it’s not available for this repository:

OptionProsConsRecommendation
A. GitHub Models APIFree, integrated, no secrets neededRequires API access enabledRecommended
B. Azure OpenAICalab.ai likely has Azure (Entra ID)Requires Azure resource + secretFallback option
C. Skip AI, deterministic onlyNo external dependencyNo semantic overlap detectionMinimal viable option

Recommendation: Start with Option A. The mece-check.mjs script has graceful degradation — if the API call fails, it returns a “manual review recommended” result instead of blocking.

Decision 2: gray-matter Dependency

The validation scripts use gray-matter for YAML frontmatter parsing. It may already be in the Quartz dependency tree.

npm ls gray-matter
ScenarioAction
Already installed (transitive dep)No action needed — import directly
Not installedAdd to workflow: npm install gray-matter in the Actions step (not local install)
Want to avoid new dependencyReplace with manual YAML parsing (regex-based, more fragile)

Recommendation: Use gray-matter and install it in the Actions workflow step. It’s the standard Node.js frontmatter parser and is lightweight.


5. Risk Assessment

RiskLikelihoodImpactMitigation
GitHub Models API not available for repoLowMediumGraceful degradation in mece-check.mjs; returns advisory warning instead of blocking
AI MECE false positivesMediumLowAdvisory only — included in PR report with “compared against” list for human verification
Issue form fields insufficient for edge casesMediumLow”Additional Context” textarea covers gaps; forms are extensible via YAML changes
CODEOWNERS teams don’t exist yetHigh (known)LowWarning in PR body; entry scaffolded but non-functional until teams are created
Workflow permissions issuesLowMediumDocumented required permissions in workflow YAML; uses standard GITHUB_TOKEN
gray-matter import issues in ESMLowLowInstall fresh in Actions step; tested in CI environment
Race condition: two scaffolding issues at onceVery LowMediumBranch names include unique VS code/practice slug; unlikely collision
Template format drift from Knowledge StandardsLowMediumScripts are the source of truth; documentation cross-references the scripts
npm install gray-matter in ActionsVery LowLowActions runs on Ubuntu (not WSL/NTFS); npm install is safe there

6. Success Criteria

Functional Criteria

  1. Creating a GitHub issue with the “Create Value Stream” form and correct inputs results in a PR with:

    • Complete folder structure (6 markdown files + metadata subdirs)
    • Correctly populated frontmatter in all files
    • CODEOWNERS entry appended
    • Validation report in PR body
    • MECE analysis in PR body
  2. Creating a GitHub issue with the “Create Management Practice” form and correct inputs results in a PR with:

    • Complete folder structure (overview + 7 artefact subdirs + metadata)
    • Correctly populated frontmatter
    • CODEOWNERS entry (if team provided)
    • Validation report in PR body
    • MECE analysis in PR body
  3. Creating an issue with invalid inputs (duplicate VS code, bad practice ID format, non-existent guild) results in:

    • A comment on the issue listing all validation failures
    • No PR created
  4. The validate-content.yml workflow catches:

    • Missing required frontmatter fields
    • Duplicate VS codes or Practice IDs
    • And reports results as a PR comment

Non-Functional Criteria

  1. Scaffolding workflow completes in under 2 minutes
  2. All documentation references to TMP01 and TMP03 are removed from content/ directory
  3. Existing Obsidian Templater workflow continues to work for TMP00, TMP02, TMP05
  4. No breaking changes to the Quartz build (npx quartz build succeeds)

7. Appendices

Appendix A: Complete File Inventory

New Files (17)

FilePurpose
.github/ISSUE_TEMPLATE/create-value-stream.ymlIssue form for VS creation
.github/ISSUE_TEMPLATE/create-management-practice.ymlIssue form for Practice creation
.github/pull_request_template.mdGeneral PR template
.github/CODEOWNERSBootstrapped CODEOWNERS file
.github/workflows/scaffold-value-stream.ymlVS scaffolding workflow
.github/workflows/scaffold-management-practice.ymlPractice scaffolding workflow
.github/workflows/validate-content.ymlContent validation CI gate
scripts/parse-issue.mjsIssue body parser utility
scripts/validate-structure.mjsDeterministic structural validation
scripts/scaffold-files.mjsFile generation (owns VS/Practice templates)
scripts/mece-check.mjsAI-powered MECE overlap detection
scripts/update-codeowners.mjsCODEOWNERS file management
scripts/validate-frontmatter.mjsFrontmatter validation for CI
content/metadata/decisions/06 Automated Content Scaffolding.mdDecision record
content/metadata/plans/08 Automated Content Scaffolding/README.mdThis plan

Deleted Files (2)

FileReason
content/metadata/templater/TMP01 Value Stream Overview.mdSuperseded by scripts/scaffold-files.mjs
content/metadata/templater/TMP03 Practice Overview.mdSuperseded by scripts/scaffold-files.mjs

Modified Files (6)

FileChanges
content/00 Governance/01 How to Contribute.mdReplace template reference with scaffolding workflow instructions; fix _templater path
content/00 Governance/04 Decision Records.mdFix TMP01 → TMP00 reference (line 62)
content/02 Guilds/GL04 Technology Guild/Practices/Solution Eng/README.mdFix TMP01 → TMP00 reference (line 66)
content/00 Governance/03 Knowledge Standards.mdAdd scaffolding workflow note under Structural Standards
docs/REPOSITORY_STRUCTURE.mdAdd scripts/ directory; update templater description
docs/CODEOWNERS_DEFERRED.mdUpdate status to reflect automation

Appendix B: Template Comparison

The following table shows the mapping from the old Obsidian Templater template fields to the new scaffolding script inputs:

TMP01 Value Stream Overview → scaffold-files.mjs vsOverviewTemplate()

Old Template FieldNew Script Input
<% tp.date.now("YYYY-MM-DD") %>new Date().toISOString().slice(0, 10)
[value-stream-owner]inputs.ownerSlug
[VS0X]inputs.code
[VS Code] [Value Stream Name]${inputs.code} ${inputs.name}
[2-3 sentences...] (Purpose)inputs.purpose
[Who receives the value]inputs.primaryCustomer
[What initiates...] (Start Trigger)inputs.startTrigger
[What completes...] (End Trigger)inputs.endTrigger
[Name/Role] (Owner)inputs.owner
@calab-ai/[vs-team-name]@calab-ai/${inputs.teamSlug}
[[Practice N]] (Related Practices)inputs.relatedPractices.map(p => \${p}`)`

TMP03 Practice Overview → scaffold-files.mjs practiceOverviewTemplate()

Old Template FieldNew Script Input
<% tp.date.now("YYYY-MM-DD") %>new Date().toISOString().slice(0, 10)
[practice-owner-role]inputs.ownerSlug
[PRACTICE-ID]inputs.id
[Guild Name]inputs.guildFolder
[Practice Name]inputs.name
[Role responsible]inputs.ownerRole
[2-3 sentences...] (Purpose)inputs.purpose
In/Out of Scope itemsinputs.inScope / inputs.outOfScope
[[Practice Name N]] (Dependencies)inputs.dependencies.map(...)
[[Practice Name N]] (Dependents)inputs.dependents.map(...)

Appendix C: Existing Content That Auto-Updates

The following content does not need manual updating when a new VS or Practice is scaffolded, because it uses page-list auto-populated listings:

FileMechanism
content/01 Value Streams/README.mdpage-list code block scans 01 Value Streams/
content/02 Guilds/README.mdpage-list code block scans 02 Guilds/
Each Guild’s Practices/README.mdpage-list code block scans the guild’s Practices/ directory

No cross-reference updates are needed in these files — the page-list plugin automatically picks up new folders at build time.

Appendix D: Troubleshooting

IssueCauseFix
Workflow doesn’t triggerIssue missing required labelEnsure issue form labels field includes value-stream or management-practice
gray-matter import failsModule not installed in ActionsVerify npm install gray-matter step runs before validation step
MECE check returns “unknown”GitHub Models API not accessibleCheck repo permissions; fallback is manual review (non-blocking)
CODEOWNERS entry not addedFile has unexpected formatCheck CODEOWNERS section markers; run update-codeowners.mjs locally to debug
PR creation failsBranch name collisionDelete stale scaffolding branches; check for existing PR
Frontmatter validation false positivesStub files without full frontmatterEnsure stub templates include minimum required fields
npm install in WSL corrupts packagesWSL + NTFS filesystem boundaryNever run npm install from WSL. The Actions workflow runs on Ubuntu (not WSL), so this only affects local development. See .opencode/skills/npm-usage/SKILL.md.

0 items under this folder.