PortfolioForge: Doc to Repo
Turn a NextWork project doc into a portfolio GitHub repo in 90 seconds.
Introduction
30 Second Summary
You have finished a dozen hands-on projects, but your GitHub profile is still empty. Turning project notes into a polished portfolio repo takes hours of writing READMEs, creating architecture diagrams, and setting up community files.
In this project, you will build PortfolioForge, a Claude Code skill that reads a NextWork project doc and generates a complete, recruiter-ready GitHub repo in under 90 seconds.
What You'll Build
PortfolioForge is a multi-agent pipeline that parses NextWork project docs, detects complexity tiers, fans out five parallel subagents to generate content, and assembles everything into a portfolio repo through a unified Copier template.
By the end of this project, you'll have:
- A Pydantic data model that parses any NextWork project doc into structured fields, and an auto-tier detector that classifies docs as intern, mid, or senior.
- A unified Copier template with tier-conditional file generation that produces READMEs, ADRs, Mermaid diagrams, CI workflows, and community-profile files.
- A 5-subagent parallel prompt architecture orchestrated by a single /forge-repo Claude Code skill, plus a 16-axis conformance checker that scores generated repos against a recruiter-signal rubric.
- Secret Mission: Add OpenSSF Scorecard, Vale prose linting, and commitlint to generated repos for production-grade quality.
Are there any prerequisites?
You need Python 3.10+ installed, a Claude Max subscription with Claude Code access, and basic familiarity with Git, Python, and the command line.
Set Up Your Environment
PortfolioForge relies on three tools working together. Copier generates entire project directories from templates. Pydantic validates the data your doc parser extracts. The GitHub CLI pushes finished repos to GitHub automatically.
In this step, you'll install all three tools and create the folder structure that organizes your project's source code, templates, and skill prompts.
In this step, get ready to:
- Install Copier and Pydantic via pip.
- Set up and authenticate the GitHub CLI.
- Scaffold the PortfolioForge project directory.
Install Copier and Pydantic
Copier is a template engine that generates entire project directories from Jinja2 templates. Pydantic is a data validation library that ensures your parsed NextWork docs match an expected schema.
Use pip3 on macOS or Linux
On macOS or Linux, you may need to use pip3 instead of pip, and python3 instead of python. Use whichever version points to Python 3.10+.
- Install both packages with pinned versions by running:
pip install copier==9.14.1 pydantic==2.13.3
What do these packages do?
copier is a project scaffolding tool. It takes a template directory full of Jinja2 files and generates a complete project from it, filling in answers to configurable questions.
pydantic enforces data contracts using Python type hints. When your parser extracts fields from a NextWork doc, Pydantic validates that every required field is present and correctly typed.
- Verify Copier installed correctly by running:
copier --version
You should see 9.14.1 in the output.
- Verify Pydantic installed correctly by running:
python -c "import pydantic; print(pydantic.__version__)"
You should see 2.13.3 in the output.
Seeing 'command not found' for copier?
Make sure you installed Copier with the same Python that your pip points to. Run pip --version to check which Python it uses.
If you have multiple Python versions, try installing again with pip3 instead of pip.
Help me troubleshoot my Copier installation
Install and authenticate the GitHub CLI
The GitHub CLI (gh) lets you create repos, open pull requests, and manage GitHub resources from the terminal. PortfolioForge uses it to push generated repos to GitHub automatically.
- Check if gh is already installed by running:
gh --version
✔️ I see a version number
The GitHub CLI is already installed. Skip ahead to the authentication step below.
ⓧ Command not found
You need to install the GitHub CLI first. Follow the instructions for your operating system.
macOS
- Install gh using Homebrew by running:
brew install gh
Don't have Homebrew?
Install Homebrew first by visiting brew.sh and following the instructions. Then re-run the command above.
Help me install Homebrew on macOS
Windows
- Install gh using winget by running:
winget install --id GitHub.cli
After the install finishes, close and reopen your terminal so the gh command becomes available.
What will the login flow look like?
The gh auth login command starts an interactive login. It asks several questions in your terminal, then opens your browser to authorize the connection.
When prompted, select GitHub.com for the account, HTTPS for the protocol, Yes to authenticate Git, and Login with a web browser for the method.
- Log in to your GitHub account by running:
gh auth login
- Follow the prompts in your terminal using the selections described above.
- When your browser opens, authorize the GitHub CLI app.
Once complete, you should see a confirmation message like Logged in as your-username.
Login not working?
If your browser does not open automatically, copy the URL shown in the terminal and paste it into your browser manually.
Make sure you are logged into the correct GitHub account in your browser before authorizing.
Help me troubleshoot my gh auth login
Create the PortfolioForge project directory
PortfolioForge's code is organized into three main areas. The src/ folder holds your Python utilities. The templates/ folder holds the Copier template. The .claude/skills/ folder holds the skill definition and subagent prompts.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the project folder and all its subdirectories by running:
macOS
mkdir -p portfolioforge/src portfolioforge/templates/portfolio-repo portfolioforge/.claude/skills/forge-repo/prompts
Windows
mkdir portfolioforge\src -Force
mkdir portfolioforge\templates\portfolio-repo -Force
mkdir portfolioforge\.claude\skills\forge-repo\prompts -Force
- Move into the new project folder by running:
cd portfolioforge
What are these directories for?
src/ stores the Python utilities you'll build: the doc parser, tier detector, and conformance checker.
templates/portfolio-repo/ holds the Copier template files that generate the final repo structure with READMEs, licenses, and community-profile files.
.claude/skills/forge-repo/prompts/ holds the 5 subagent prompt files that Claude Code's Task tool reads at runtime to run parallel agents.
- Confirm the directories were created by running:
ls
You should see src and templates listed in the output.
Where is the .claude folder?
Directories starting with a dot are hidden by default. Run ls -a on macOS/Linux or dir -Force on Windows to reveal it.
- Create a new file called requirements.txt in your portfolioforge folder with the following content:
pydantic==2.13.3
copier==9.14.1
What is requirements.txt?
This file pins the exact versions of your Python dependencies. Anyone who clones your project can run pip install -r requirements.txt to get the same packages you used.
- Save requirements.txt.
- Confirm the file exists by running:
ls
You should see requirements.txt, src, and templates in the output.
Missing files or directories?
Double check that you ran the mkdir command from your Desktop, not from inside another folder.
Make sure you are inside the portfolioforge folder when running ls. Run pwd to confirm your current location.
Help me verify my PortfolioForge project structure
✔️ Awesome, I've got everything!
Double check you've saved your file and move on to the validation below.
ⓧ I'd like to double check the full code
Here is what requirements.txt should look like:
pydantic==2.13.3
copier==9.14.1
Your environment is locked and loaded. Next up, you'll build the doc parser and tier detector that turn raw NextWork markdown into structured data.
Build the NextWork Doc Reader and Tier Detector
In Step 1, you built the project skeleton and installed all the dependencies. Now it's time to teach PortfolioForge how to read.
Every NextWork project doc follows the same pattern: an introduction, tools and concepts, project reflection, and step-by-step walkthroughs. If you can extract those four sections into structured data, you can feed them to specialized AI subagents later. In this step, you'll build a Pydantic-powered parser that extracts these sections from raw markdown, plus a tier detector that classifies projects by complexity.
In this step, get ready to:
- Create a Pydantic model that defines the schema for parsed NextWork docs.
- Build parsing functions that extract structured sections from raw markdown.
- Add a tier detector that classifies projects as intern, mid, or senior.
Define the NextWorkDoc schema
Before PortfolioForge can do anything useful with a NextWork doc, it needs a clear data contract. What fields does a parsed doc have? What types are they? Pydantic lets you define this contract as a Python class that automatically validates data on creation.
- Create a new file called nextwork_doc.py inside your src/ directory by running:
touch src/nextwork_doc.py
- Open src/nextwork_doc.py in your editor and paste the following imports and model definition:
"""NextWork document parser. Extracts structured fields from NextWork project markdown."""
import re
import sys
from pathlib import Path
from pydantic import BaseModel, Field
class NextWorkDoc(BaseModel):
"""Schema for a parsed NextWork project document."""
title: str = Field(description="Project title from the first heading")
project_slug: str = Field(description="URL-safe project name slug")
author: str = Field(default="NextWork Learner", description="Author name")
introducing: str = Field(default="", description="Introducing Today's Project section")
tools_and_concepts: str = Field(default="", description="Tools and concepts section")
project_reflection: str = Field(default="", description="Project reflection section")
walkthroughs: list[str] = Field(default_factory=list, description="Per-step walkthrough sections")
source_file: str = Field(description="Path to the source file")
chain_files: list[str] = Field(default_factory=list, description="Paths for sequential chain files")
What does this code do?
The NextWorkDoc class is a Pydantic BaseModel that defines every field a parsed doc can have. Pydantic validates types automatically, so if you accidentally pass a number where a string is expected, you'll get an immediate error.
- The four core sections (introducing, tools_and_concepts, project_reflection, walkthroughs) map to the standard sections every NextWork doc contains.
- Fields with default or default_factory are optional. Fields without defaults (like title and source_file) are required.
- chain_files stores paths when a project spans multiple sequential docs. The subagents will use this later to know the full scope of the project.
You won't see output yet. The next chunk adds the functions that populate this model.
Add the parsing functions
With the schema defined, you need functions that actually read markdown files and extract the right sections. The parser uses regex patterns to find NextWork's standard headings and pull out the content beneath them.
- In src/nextwork_doc.py, add the following helper functions and parse_doc() below the NextWorkDoc class:
def slugify(text: str) -> str:
"""Convert text to a URL-safe slug."""
text = text.lower().strip()
text = re.sub(r"[^\w\s-]", "", text)
text = re.sub(r"[\s_]+", "-", text)
text = re.sub(r"-+", "-", text)
return text.strip("-")
def extract_section(content: str, heading_pattern: str) -> str:
"""Extract content under a heading matching the pattern until the next same-level heading."""
pattern = rf"(^#{1,3}\s+.*{heading_pattern}.*$)(.*?)(?=^#{1,3}\s|\Z)"
match = re.search(pattern, content, re.MULTILINE | re.DOTALL | re.IGNORECASE)
if match:
return match.group(2).strip()
return ""
def extract_walkthroughs(content: str) -> list[str]:
"""Extract individual step walkthrough sections."""
pattern = r"(^#{1,3}\s+(?:Step|Part)\s+\d+.*$)(.*?)(?=^#{1,3}\s+(?:Step|Part)\s+\d+|\Z)"
matches = re.findall(pattern, content, re.MULTILINE | re.DOTALL | re.IGNORECASE)
return [f"{heading.strip()}\n{body.strip()}" for heading, body in matches if body.strip()]
def parse_doc(file_path: str) -> NextWorkDoc:
"""Parse a single NextWork document into structured fields."""
path = Path(file_path)
content = path.read_text(encoding="utf-8")
title_match = re.search(r"^#\s+(.+)$", content, re.MULTILINE)
title = title_match.group(1).strip() if title_match else path.stem
return NextWorkDoc(
title=title,
project_slug=slugify(title),
introducing=extract_section(content, r"Introducing\s+Today"),
tools_and_concepts=extract_section(content, r"Tools\s+and\s+[Cc]oncepts"),
project_reflection=extract_section(content, r"Project\s+[Rr]eflection"),
walkthroughs=extract_walkthroughs(content),
source_file=str(path),
)
What does this code do?
- slugify() converts a project title like "My S3 Website" into a URL-safe slug like my-s3-website. This becomes the repo name.
- extract_section() uses a regex to find a heading matching a pattern (like "Introducing Today") and grabs everything beneath it until the next same-level heading.
- extract_walkthroughs() finds all headings that start with "Step" or "Part" followed by a number, capturing each walkthrough as a separate string.
- parse_doc() ties it all together. It reads a file, extracts the title from the first # heading, then calls the helpers to populate every field in the NextWorkDoc schema.
You won't be able to run the file yet. The next chunk adds the chain parser and the entry point.
- Still in src/nextwork_doc.py, add the parse_chain() function and the __main__ entry point below parse_doc():
def parse_chain(directory: str) -> NextWorkDoc:
"""Parse a directory of sequential NextWork docs into one merged document."""
dir_path = Path(directory)
md_files = sorted(dir_path.glob("*.md"))
if not md_files:
raise FileNotFoundError(f"No markdown files found in {directory}")
docs = [parse_doc(str(f)) for f in md_files]
primary = docs[0]
merged_walkthroughs = []
for doc in docs:
merged_walkthroughs.extend(doc.walkthroughs)
all_reflections = "\n\n".join(d.project_reflection for d in docs if d.project_reflection)
all_tools = "\n\n".join(d.tools_and_concepts for d in docs if d.tools_and_concepts)
return NextWorkDoc(
title=primary.title,
project_slug=primary.project_slug,
author=primary.author,
introducing=primary.introducing,
tools_and_concepts=all_tools,
project_reflection=all_reflections,
walkthroughs=merged_walkthroughs,
source_file=str(md_files[0]),
chain_files=[str(f) for f in md_files],
)
if __name__ == "__main__":
import json
target = sys.argv[1]
path = Path(target)
if path.is_dir():
doc = parse_chain(target)
else:
doc = parse_doc(target)
print(json.dumps(doc.model_dump(), indent=2))
What does this code do?
- parse_chain() handles multi-file projects. It reads every .md file in a directory, parses each one, then merges their walkthroughs, reflections, and tool lists into a single NextWorkDoc.
- The __main__ block lets you run the file as a script. Pass a file path for a single doc, or a directory for a chain. It prints the parsed result as JSON.
- model_dump() is Pydantic's method for converting a model instance into a plain dictionary. This is how the subagents will receive the parsed data later.
Time to verify the parser works. You'll create a small test markdown file and run the parser on it.
- Create a test-docs/ directory and a sample markdown file by running:
mkdir -p test-docs
- Create a file called test-docs/sample.md in your editor and paste the following content:
# My S3 Website Project
## Introducing Today's Project
I built a static website hosted on Amazon S3.
## Tools and Concepts
S3, HTML, CloudFront, Route 53
## Project Reflection
I learned how to configure S3 bucket policies and set up CloudFront.
## Step 1: Create the Bucket
First I created the S3 bucket with public access.
## Step 2: Upload Files
Then I uploaded my HTML files to the bucket.
- Save src/nextwork_doc.py and test-docs/sample.md.
- Run the parser on your test file by running:
python src/nextwork_doc.py test-docs/sample.md
- You should see a JSON object with all the fields from your NextWorkDoc schema. The title field should read My S3 Website Project, project_slug should be my-s3-website-project, and walkthroughs should contain two items.
Seeing a ModuleNotFoundError?
- If you see ModuleNotFoundError: No module named 'pydantic', make sure you installed the dependencies from Step 1. Run pip install pydantic to install it.
- If you see FileNotFoundError, double check that you're running the command from inside the portfolioforge/ directory and that test-docs/sample.md exists.
- If you see empty strings for introducing or other sections, make sure the headings in your test file match exactly (e.g. "Introducing Today's Project", not "Introduction").
- Why isn't my parser extracting sections correctly?
✔️ Awesome, I've got everything!
Make sure you've saved src/nextwork_doc.py.
ⓧ I'd like to double check the full code
"""NextWork document parser. Extracts structured fields from NextWork project markdown."""
import re
import sys
from pathlib import Path
from pydantic import BaseModel, Field
class NextWorkDoc(BaseModel):
"""Schema for a parsed NextWork project document."""
title: str = Field(description="Project title from the first heading")
project_slug: str = Field(description="URL-safe project name slug")
author: str = Field(default="NextWork Learner", description="Author name")
introducing: str = Field(default="", description="Introducing Today's Project section")
tools_and_concepts: str = Field(default="", description="Tools and concepts section")
project_reflection: str = Field(default="", description="Project reflection section")
walkthroughs: list[str] = Field(default_factory=list, description="Per-step walkthrough sections")
source_file: str = Field(description="Path to the source file")
chain_files: list[str] = Field(default_factory=list, description="Paths for sequential chain files")
def slugify(text: str) -> str:
"""Convert text to a URL-safe slug."""
text = text.lower().strip()
text = re.sub(r"[^\w\s-]", "", text)
text = re.sub(r"[\s_]+", "-", text)
text = re.sub(r"-+", "-", text)
return text.strip("-")
def extract_section(content: str, heading_pattern: str) -> str:
"""Extract content under a heading matching the pattern until the next same-level heading."""
pattern = rf"(^#{1,3}\s+.*{heading_pattern}.*$)(.*?)(?=^#{1,3}\s|\Z)"
match = re.search(pattern, content, re.MULTILINE | re.DOTALL | re.IGNORECASE)
if match:
return match.group(2).strip()
return ""
def extract_walkthroughs(content: str) -> list[str]:
"""Extract individual step walkthrough sections."""
pattern = r"(^#{1,3}\s+(?:Step|Part)\s+\d+.*$)(.*?)(?=^#{1,3}\s+(?:Step|Part)\s+\d+|\Z)"
matches = re.findall(pattern, content, re.MULTILINE | re.DOTALL | re.IGNORECASE)
return [f"{heading.strip()}\n{body.strip()}" for heading, body in matches if body.strip()]
def parse_doc(file_path: str) -> NextWorkDoc:
"""Parse a single NextWork document into structured fields."""
path = Path(file_path)
content = path.read_text(encoding="utf-8")
title_match = re.search(r"^#\s+(.+)$", content, re.MULTILINE)
title = title_match.group(1).strip() if title_match else path.stem
return NextWorkDoc(
title=title,
project_slug=slugify(title),
introducing=extract_section(content, r"Introducing\s+Today"),
tools_and_concepts=extract_section(content, r"Tools\s+and\s+[Cc]oncepts"),
project_reflection=extract_section(content, r"Project\s+[Rr]eflection"),
walkthroughs=extract_walkthroughs(content),
source_file=str(path),
)
def parse_chain(directory: str) -> NextWorkDoc:
"""Parse a directory of sequential NextWork docs into one merged document."""
dir_path = Path(directory)
md_files = sorted(dir_path.glob("*.md"))
if not md_files:
raise FileNotFoundError(f"No markdown files found in {directory}")
docs = [parse_doc(str(f)) for f in md_files]
primary = docs[0]
merged_walkthroughs = []
for doc in docs:
merged_walkthroughs.extend(doc.walkthroughs)
all_reflections = "\n\n".join(d.project_reflection for d in docs if d.project_reflection)
all_tools = "\n\n".join(d.tools_and_concepts for d in docs if d.tools_and_concepts)
return NextWorkDoc(
title=primary.title,
project_slug=primary.project_slug,
author=primary.author,
introducing=primary.introducing,
tools_and_concepts=all_tools,
project_reflection=all_reflections,
walkthroughs=merged_walkthroughs,
source_file=str(md_files[0]),
chain_files=[str(f) for f in md_files],
)
if __name__ == "__main__":
import json
target = sys.argv[1]
path = Path(target)
if path.is_dir():
doc = parse_chain(target)
else:
doc = parse_doc(target)
print(json.dumps(doc.model_dump(), indent=2))
Create the tier detector
Different NextWork projects produce different numbers of files. A simple introductory project is often a single markdown doc. A complex multi-part series might span 5 or more sequential files. The tier detector uses this file count as a signal to classify projects into three levels: intern, mid, and senior.
- Create a new file called tier_detector.py inside your src/ directory by running:
touch src/tier_detector.py
- Open src/tier_detector.py and paste the following:
"""Auto-tier detection for NextWork project documents."""
import sys
from pathlib import Path
def detect_tier(target: str) -> str:
"""Detect the project tier based on input shape.
Rules:
- Single loose file: intern
- 2-4 sequential files in a directory: mid
- 5+ sequential files in a directory: senior
"""
path = Path(target)
if path.is_file():
return "intern"
if path.is_dir():
md_files = sorted(path.glob("*.md"))
count = len(md_files)
if count <= 1:
return "intern"
elif count <= 4:
return "mid"
else:
return "senior"
return "intern"
if __name__ == "__main__":
target = sys.argv[1]
tier = detect_tier(target)
print(tier)
What does this code do?
The detect_tier() function uses a simple decision tree based on file count.
- If the input is a single file, it's an intern tier project (1 doc, basic repo output).
- If the input is a directory with 2-4 markdown files, it's a mid tier project (adds ADRs, diagrams, CI, and community files).
- If the directory has 5 or more files, it's a senior tier project (adds phase-split tutorials, issue templates, and the full community profile).
The Copier template you'll build in the next step uses this tier string to decide which files to generate.
- Save src/tier_detector.py.
- Test the tier detector on your single test file by running:
python src/tier_detector.py test-docs/sample.md
- You should see intern printed to the terminal, because you passed a single file.
- Now test it on the test-docs/ directory by running:
python src/tier_detector.py test-docs/
- You should see intern again, because the directory contains only 1 markdown file.
Seeing something other than intern?
- If you see mid or senior when running on the directory, check that test-docs/ only contains your single sample.md file. Extra .md files in the directory will increase the count.
- If you see an IndexError, make sure you're passing the file path or directory path as an argument after the script name.
- Why is my tier detector returning the wrong tier?
✔️ Awesome, I've got everything!
Make sure you've saved src/tier_detector.py.
ⓧ I'd like to double check the full code
"""Auto-tier detection for NextWork project documents."""
import sys
from pathlib import Path
def detect_tier(target: str) -> str:
"""Detect the project tier based on input shape.
Rules:
- Single loose file: intern
- 2-4 sequential files in a directory: mid
- 5+ sequential files in a directory: senior
"""
path = Path(target)
if path.is_file():
return "intern"
if path.is_dir():
md_files = sorted(path.glob("*.md"))
count = len(md_files)
if count <= 1:
return "intern"
elif count <= 4:
return "mid"
else:
return "senior"
return "intern"
if __name__ == "__main__":
target = sys.argv[1]
tier = detect_tier(target)
print(tier)
Your doc reader and tier detector are ready. PortfolioForge can now parse any NextWork doc into structured data and classify it by complexity. Next up, you'll build the Copier template that turns this structured data into actual repo files.
Create the Copier Template Foundation
Your doc reader can parse any NextWork markdown into structured fields, and your tier detector knows whether a project is intern, mid, or senior.
Now you need something that turns those structured fields into actual files on disk. Copier is a project-templating tool that reads a configuration file of questions, combines the answers with Jinja2 template files, and generates a complete directory of rendered output.
In this step, get ready to:
- Write the Copier template configuration with questions for every field your subagents will fill.
- Create a README template that follows a cognitive-funnel layout with tier-conditional sections.
- Add the base files every tier gets: LICENSE, .gitignore, credit attribution, and contribution guide.
Write the Copier template configuration
Every Copier template starts with a copier.yml file that defines the questions (variables) the template expects.
Your 5 subagents will each produce a piece of content. The copier.yml is the contract that maps each subagent's output to a named template variable.
- Create a new file called copier.yml inside your templates/portfolio-repo/ directory.
- Copy the following configuration into the file:
# PortfolioForge Copier Template Configuration
_min_copier_version: "9.10"
_templates_suffix: ".jinja"
project_name:
type: str
help: What is the project name?
project_slug:
type: str
help: URL-safe project slug
default: "{{ project_name | lower | replace(' ', '-') | regex_replace('[^a-z0-9-]', '') }}"
description:
type: str
help: One-line project description
default: "A NextWork portfolio project."
tier:
type: str
help: Project tier
choices:
- intern
- mid
- senior
default: intern
author:
type: str
help: Author name
default: "NextWork Learner"
year:
type: str
help: Copyright year
default: "2026"
nextwork_source:
type: str
help: NextWork project source reference
default: ""
tools_used:
type: str
help: Comma-separated list of tools used
default: ""
readme_content:
type: str
help: Main README body content (from structurer agent)
default: ""
adr_content:
type: str
help: ADR content in JSON format (from ADR-author agent)
default: "[]"
diagram_content:
type: str
help: Mermaid diagram code (from diagram-author agent)
default: ""
credit_content:
type: str
help: NextWork credit block (from credit-writer agent)
default: ""
phase_content:
type: str
help: Phase-split content in JSON format (from phase-splitter agent)
default: "[]"
What does this configuration do?
The top two settings control Copier itself. _min_copier_version ensures anyone running this template has at least version 9.10. _templates_suffix tells Copier to process any file ending in .jinja through the Jinja2 engine and strip the suffix from the output filename.
The first group of questions (project_name through nextwork_source) captures project metadata. The second group (readme_content through phase_content) holds the content each subagent generates. Copier's --data CLI flag lets the orchestrator inject these values without interactive prompts.
- Save copier.yml.
- Confirm the file exists by running:
ls templates/portfolio-repo/
You should see copier.yml in the output.
Don't see copier.yml?
Make sure you created the file inside templates/portfolio-repo/, not in the project root or in src/. The path from your portfolioforge/ directory should be templates/portfolio-repo/copier.yml.
Help me find where I saved copier.yml
Why a single template instead of three?
You could create separate templates for intern, mid, and senior tiers. A single template with tier-conditional logic is easier to maintain, avoids duplicating shared files, and teaches a deeper Copier pattern.
The tier question you just defined is the toggle. Every template file can check its value to decide what to include.
Create the README template
The README is the most important file in any portfolio repo. Recruiters spend about 10 seconds scanning it before deciding whether to read further.
Your README template uses a cognitive-funnel design: the most important information appears first, and each section targets a progressively more technical reader.
- Create a new file called README.md.jinja inside templates/portfolio-repo/.
- Copy the following template into the file:
# {{ project_name }}
{% if tier != 'intern' %} {% endif %} 
> {{ description }}
{% if tier != 'intern' %}
## Who This Is For
| Reader | What to look at | Time |
|--------|----------------|------|
| Recruiter | This README, badges, demo screenshot | 10 sec |
| Hiring Manager | ADR links below, project scope | 2 min |
| Engineer | Source code, architecture diagram | 15 min |
| Learner | Full walkthrough in docs/tutorials/ | 30 min |
{% endif %}
## What This Project Does
{{ readme_content }}
{% if tier != 'intern' and diagram_content %}
## Architecture
```mermaid
{{ diagram_content }}
```
{% endif %}
{% if tier != 'intern' %}
## Key Decisions
See the [Architecture Decision Records](docs/decisions/) for the reasoning behind major technical choices.
{% endif %}
## Built With
{% for tool in tools_used.split(',') %}{% if tool.strip() %}- {{ tool.strip() }}
{% endif %}{% endfor %}
---
{{ credit_content }}
*This repo was generated by [PortfolioForge](https://github.com/portfolioforge) from a NextWork project doc.*
What is the cognitive-funnel layout?
- The hero section (title, badges, one-liner) targets recruiters who spend 10 seconds scanning.
- The KPI/audience table (mid and senior tiers only) routes different readers to the sections that matter to them.
- The architecture diagram and ADR links appear only for non-intern tiers, since simpler projects do not need them.
- The {% if tier != 'intern' %} blocks are Jinja2 conditionals. Copier evaluates them at generation time and omits the entire section when the condition is false.
- Save README.md.jinja.
- Confirm it exists by running:
ls templates/portfolio-repo/
You should now see both copier.yml and README.md.jinja listed in the directory.
Seeing a .jinja.jinja extension?
The file must be named README.md.jinja (one .jinja suffix). Copier strips the .jinja suffix during generation, producing README.md in the output. A double suffix like README.md.jinja.jinja would produce README.md.jinja instead of the expected output.
Help me fix my README template filename
Create the base template files
Every portfolio repo needs a handful of standard files regardless of tier. These include a license, a .gitignore, and proper attribution back to the original NextWork project.
- Create a new file called LICENSE.jinja inside templates/portfolio-repo/ and copy the following into it:
MIT License
Copyright (c) {{ year }} {{ author }}
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
What does this file do?
This is a standard MIT License with two Jinja2 variables: {{ year }} and {{ author }}. Copier replaces them with the values from copier.yml at generation time, producing a personalized license file.
- Save LICENSE.jinja.
- Next, create a file called .gitignore (no .jinja suffix, since this file has no template variables) in the same directory and copy the following into it:
__pycache__/
*.py[cod]
*$py.class
*.so
.env
.venv/
venv/
*.egg-info/
dist/
build/
.idea/
.vscode/
*.swp
*.swo
.DS_Store
Thumbs.db
- Save .gitignore.
- Confirm both files exist by running:
ls -a templates/portfolio-repo/
You should see LICENSE.jinja and .gitignore alongside the files from earlier. The -a flag shows hidden files like .gitignore.
Don't see .gitignore in the listing?
Files starting with a dot are hidden by default. Make sure you used ls -a (with the -a flag) to display them. Also check that the file is named .gitignore with the leading dot, not just gitignore.
Help me find my .gitignore file
Now you need three more files: the NextWork credit attribution, a contribution guide, and a Copier answers tracker.
- Create nextwork-credit.md.jinja inside templates/portfolio-repo/ and copy the following into it:
# NextWork Project Credit
This portfolio repository was built from a [NextWork](https://www.nextwork.org/) project.
- **Original Project**: {{ project_name }}
- **NextWork Source**: {{ nextwork_source if nextwork_source else "NextWork Project Library" }}
- **Author**: {{ author }}
- **Tier**: {{ tier }}
{{ credit_content }}
## About NextWork
NextWork provides hands-on cloud and AI projects that help learners build real skills. Learn more at [nextwork.org](https://www.nextwork.org/).
What does this file do?
This file gives proper credit to the original NextWork project. The {{ nextwork_source if nextwork_source else "NextWork Project Library" }} line is a Jinja2 inline conditional. It displays the source reference if one was provided, or falls back to a default string.
The {{ credit_content }} placeholder is where the credit-writer subagent's output gets injected later.
- Save nextwork-credit.md.jinja.
- Create CONTRIBUTING.md.jinja in the same directory and copy the following into it:
# Contributing to {{ project_name }}
Thank you for your interest in contributing to this project.
## How to Contribute
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/your-feature`)
3. Commit your changes using [Conventional Commits](https://www.conventionalcommits.org/)
4. Push to the branch (`git push origin feature/your-feature`)
5. Open a Pull Request
## Code of Conduct
{% if tier != 'intern' %}Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing.{% else %}Be respectful and constructive in all interactions.{% endif %}
Why is the Code of Conduct section conditional?
Mid and senior tier repos will include a separate CODE_OF_CONDUCT.md file (you will create that in the next step). The conditional links to it for those tiers, but falls back to a simple inline statement for intern tier where no separate file exists.
- Save CONTRIBUTING.md.jinja.
- Create one more file called {{_copier_conf.answers_file}}.jinja in the same directory and copy the following into it:
# Changes here will be overwritten by Copier
{{ _copier_answers|to_nice_yaml -}}
What is the answers file?
Copier uses this special file to track which answers were used during generation. The filename itself is a Jinja2 expression ({{_copier_conf.answers_file}}) that Copier resolves at generation time. Inside, the to_nice_yaml filter converts all the answers into readable YAML.
- Save the file.
- Confirm all your base files exist by running:
ls -a templates/portfolio-repo/
You should see 7 files: copier.yml, README.md.jinja, LICENSE.jinja, .gitignore, nextwork-credit.md.jinja, CONTRIBUTING.md.jinja, and the answers file.
Missing one or more files?
Double-check that every file is inside templates/portfolio-repo/. A common mistake is accidentally saving files one level up in templates/ or in the project root. If a file is missing, create it again in the correct directory.
Help me find my missing template files
✔️ Awesome, I've got everything!
Double check you have saved all 7 files in templates/portfolio-repo/.
ⓧ I'd like to double check the full code
Here is every file you should have in templates/portfolio-repo/ at this point. Compare each one against your version.
# PortfolioForge Copier Template Configuration
_min_copier_version: "9.10"
_templates_suffix: ".jinja"
project_name:
type: str
help: What is the project name?
project_slug:
type: str
help: URL-safe project slug
default: "{{ project_name | lower | replace(' ', '-') | regex_replace('[^a-z0-9-]', '') }}"
description:
type: str
help: One-line project description
default: "A NextWork portfolio project."
tier:
type: str
help: Project tier
choices:
- intern
- mid
- senior
default: intern
author:
type: str
help: Author name
default: "NextWork Learner"
year:
type: str
help: Copyright year
default: "2026"
nextwork_source:
type: str
help: NextWork project source reference
default: ""
tools_used:
type: str
help: Comma-separated list of tools used
default: ""
readme_content:
type: str
help: Main README body content (from structurer agent)
default: ""
adr_content:
type: str
help: ADR content in JSON format (from ADR-author agent)
default: "[]"
diagram_content:
type: str
help: Mermaid diagram code (from diagram-author agent)
default: ""
credit_content:
type: str
help: NextWork credit block (from credit-writer agent)
default: ""
phase_content:
type: str
help: Phase-split content in JSON format (from phase-splitter agent)
default: "[]"
# {{ project_name }}
{% if tier != 'intern' %} {% endif %} 
> {{ description }}
{% if tier != 'intern' %}
## Who This Is For
| Reader | What to look at | Time |
|--------|----------------|------|
| Recruiter | This README, badges, demo screenshot | 10 sec |
| Hiring Manager | ADR links below, project scope | 2 min |
| Engineer | Source code, architecture diagram | 15 min |
| Learner | Full walkthrough in docs/tutorials/ | 30 min |
{% endif %}
## What This Project Does
{{ readme_content }}
{% if tier != 'intern' and diagram_content %}
## Architecture
```mermaid
{{ diagram_content }}
```
{% endif %}
{% if tier != 'intern' %}
## Key Decisions
See the [Architecture Decision Records](docs/decisions/) for the reasoning behind major technical choices.
{% endif %}
## Built With
{% for tool in tools_used.split(',') %}{% if tool.strip() %}- {{ tool.strip() }}
{% endif %}{% endfor %}
---
{{ credit_content }}
*This repo was generated by [PortfolioForge](https://github.com/portfolioforge) from a NextWork project doc.*
MIT License
Copyright (c) {{ year }} {{ author }}
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
__pycache__/
*.py[cod]
*$py.class
*.so
.env
.venv/
venv/
*.egg-info/
dist/
build/
.idea/
.vscode/
*.swp
*.swo
.DS_Store
Thumbs.db
# NextWork Project Credit
This portfolio repository was built from a [NextWork](https://www.nextwork.org/) project.
- **Original Project**: {{ project_name }}
- **NextWork Source**: {{ nextwork_source if nextwork_source else "NextWork Project Library" }}
- **Author**: {{ author }}
- **Tier**: {{ tier }}
{{ credit_content }}
## About NextWork
NextWork provides hands-on cloud and AI projects that help learners build real skills. Learn more at [nextwork.org](https://www.nextwork.org/).
# Contributing to {{ project_name }}
Thank you for your interest in contributing to this project.
## How to Contribute
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/your-feature`)
3. Commit your changes using [Conventional Commits](https://www.conventionalcommits.org/)
4. Push to the branch (`git push origin feature/your-feature`)
5. Open a Pull Request
## Code of Conduct
{% if tier != 'intern' %}Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing.{% else %}Be respectful and constructive in all interactions.{% endif %}
# Changes here will be overwritten by Copier
{{ _copier_answers|to_nice_yaml -}}
Your template foundation is in place. Every generated repo will get a professional README, proper licensing, attribution, and a contribution guide straight out of the box. Next up, you will add the tier-conditional files that make mid and senior repos stand out.
Add Tier-Conditional Content to the Template
Your base template files give every repo a solid foundation. But a senior-level portfolio repo needs more than a README and a license.
Copier supports conditional file generation through Jinja2 syntax embedded directly in filenames. In this step, you will use that pattern to make entire files and directories appear or disappear based on the project tier.
In this step, get ready to:
- Add community-profile files that only appear for mid and senior tiers.
- Create a tier-conditional docs directory with ADR and tutorial structures.
- Add a GitHub Actions CI workflow and community templates for higher tiers.
Add community-profile files with conditional filenames
A professional GitHub repo includes community-health files like a Code of Conduct and a Security Policy. Interns rarely need these, but mid and senior-tier repos should have them.
Copier handles this with conditional filenames. You wrap the filename itself in a Jinja2 {% if %} block. When the condition is false, the filename evaluates to an empty string and Copier skips the file entirely.
- Inside your templates/portfolio-repo/ folder, create a new file with this exact filename (including the Jinja2 syntax):
{% if tier != 'intern' %}CODE_OF_CONDUCT.md{% endif %}.jinja
- Paste this content into the file:
# Code of Conduct
## Our Pledge
We pledge to make participation in this project a harassment-free experience for everyone.
## Our Standards
Examples of behavior that contributes to a positive environment:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints
- Gracefully accepting constructive criticism
## Enforcement
Instances of unacceptable behavior may be reported to the project maintainer.
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
How does the conditional filename work?
The {% if tier != 'intern' %} wrapper around CODE_OF_CONDUCT.md tells Copier to evaluate the filename as a Jinja2 expression. If the tier is mid or senior, the filename resolves to CODE_OF_CONDUCT.md and the file gets created.
If the tier is intern, the filename resolves to an empty string. Copier sees nothing to create and skips the file. The .jinja suffix stays outside the conditional so Copier still recognizes it as a template.
- Save the file.
You will not see a visible change yet. The next file uses the same pattern and we will verify both together.
- Create a second file in the same templates/portfolio-repo/ folder with this filename:
{% if tier != 'intern' %}SECURITY.md{% endif %}.jinja
- Paste this content into the file:
# Security Policy
## Reporting a Vulnerability
If you discover a security vulnerability in this project, please report it responsibly.
**Do not open a public issue.** Instead, email the maintainer directly.
## Supported Versions
| Version | Supported |
|---------|-----------|
| Latest | Yes |
Why include a Security Policy?
GitHub looks for a SECURITY.md file and displays it on the repo's Security tab. Having one signals to recruiters and hiring managers that you think about software security, even in a portfolio project.
- Save SECURITY.md.
- Confirm both files exist by running this from your portfolioforge folder:
ls templates/portfolio-repo/
You should see both new files with the full Jinja2 conditional syntax in their filenames, alongside the base files from the previous step.
Don't see the conditional filenames?
Make sure the entire {% if tier != 'intern' %}CODE_OF_CONDUCT.md{% endif %}.jinja string is the filename, not a folder path. The .jinja suffix must sit outside the {% endif %} tag.
If your editor created a folder instead of a file, delete it and try again. Some editors interpret % or { characters differently. Creating the file from the terminal with touch in double quotes can help.
Help me fix my conditional Copier filenames.
Create the tier-conditional docs directory
Conditional filenames work on directories too. A mid-tier repo needs an Architecture Decision Records folder. A senior-tier repo also needs a tutorials folder for phase-split documentation.
What are Architecture Decision Records?
ADRs are short documents that capture the reasoning behind a technical decision. They follow the Nygard format: title, status, context, decision, and consequences.
Hiring managers love seeing ADRs in a portfolio repo because they prove you can articulate tradeoffs, not just write code.
- Create the conditional docs directory structure by running these commands from your portfolioforge folder:
mkdir -p "templates/portfolio-repo/{% if tier != 'intern' %}docs{% endif %}/decisions"
mkdir -p "templates/portfolio-repo/{% if tier != 'intern' %}docs{% endif %}/{% if tier == 'senior' %}tutorials{% endif %}"
What do these directories do?
The first command creates a docs/decisions/ folder that only appears for mid and senior tiers. The {% if tier != 'intern' %}docs{% endif %}/ wrapper means the entire docs/ directory name evaluates to empty for intern tier.
The second command nests a second conditional inside the first. The tutorials/ subdirectory only appears for senior tier because it uses {% if tier == 'senior' %}. Note that directories do NOT get the .jinja suffix.
- Confirm the directory tree was created by running:
find templates/portfolio-repo -type d
You should see both the decisions/ and tutorials/ directories nested inside the conditional docs path.
Now add an ADR template file inside the decisions/ directory. This file gives each generated repo a starter ADR that the ADR-author subagent will populate later.
- Create a file called 001-record.md.jinja inside the decisions/ directory and paste this content:
# Architecture Decision Record
{{ adr_content }}
Why is the ADR template so short?
The {{ adr_content }} variable receives its full content from the ADR-author subagent you will build in Step 5. The template is just a vessel for that injected content.
- Save 001-record.md.jinja.
Add GitHub Actions and community templates
Mid and senior-tier repos should have a CI workflow that validates the repo structure on every push. Senior-tier repos also get issue and PR templates that signal engineering maturity.
- Create the conditional .github directory structure by running these commands:
mkdir -p "templates/portfolio-repo/{% if tier != 'intern' %}.github{% endif %}/workflows"
mkdir -p "templates/portfolio-repo/.github/ISSUE_TEMPLATE"
What does this directory structure do?
The first command creates a .github/workflows/ path that only appears for mid and senior tiers. The conditional wraps the .github directory name itself.
The second command creates a separate .github/ISSUE_TEMPLATE/ path for senior-tier issue templates. The files inside this directory will handle the senior-only condition.
Now create the GitHub Actions CI workflow. This is the most substantial template file in this substep.
- Create a file called ci.yml.jinja inside the conditional workflows/ directory and paste this content:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate README exists
run: test -f README.md
- name: Validate LICENSE exists
run: test -f LICENSE
- name: Check markdown links
uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: "yes"
What does this CI workflow do?
This workflow runs on every push and pull request to the main branch. It validates that the repo has a README.md and LICENSE file, then checks all markdown links for broken URLs.
The permissions: contents: read line follows the principle of least privilege. The workflow only needs to read files, so it only requests read access.
- Save ci.yml.jinja.
Finally, add the senior-tier community templates. These are lightweight files that GitHub displays on the repo's issue and PR creation pages.
- Create a file called bug_report.md.jinja inside .github/ISSUE_TEMPLATE/ and paste this content:
---
name: Bug Report
about: Report a bug in this project
labels: bug
---
## Describe the Bug
A clear description of what the bug is.
## Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. See error
## Expected Behavior
What you expected to happen.
## Environment
- OS: [e.g. macOS, Windows]
- Version: [e.g. 1.0.0]
- Create a file called PULL_REQUEST_TEMPLATE.md.jinja inside the .github/ directory (not inside ISSUE_TEMPLATE/) and paste this content:
## What This PR Does
Brief description of the changes.
## How to Test
1. Step one
2. Step two
## Checklist
- [ ] README updated (if needed)
- [ ] Tests pass
- [ ] No new warnings
- Save both files.
- Verify the full template directory structure by running:
find templates/portfolio-repo -type f -o -type d | sort
You should see the conditional directories (with {% if %} syntax in their names), the CI workflow inside workflows/, the ADR template inside decisions/, and the issue/PR templates inside .github/.
Missing directories or files in the output?
If your find output looks shorter than expected, check that you ran the mkdir -p commands from the portfolioforge root folder. Running from a subfolder creates the directories in the wrong location.
Also verify the quotes around the mkdir paths. The % and { characters must be enclosed in double quotes so your shell treats them as literal characters.
Help me troubleshoot my Copier template directory structure.
✔️ Awesome, I've got everything!
Great. Double check you have saved all your new files before moving on.
ⓧ I'd like to double check the full code
Here is the content for each new file created in this step. Compare yours against these.
# Code of Conduct
## Our Pledge
We pledge to make participation in this project a harassment-free experience for everyone.
## Our Standards
Examples of behavior that contributes to a positive environment:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints
- Gracefully accepting constructive criticism
## Enforcement
Instances of unacceptable behavior may be reported to the project maintainer.
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
# Security Policy
## Reporting a Vulnerability
If you discover a security vulnerability in this project, please report it responsibly.
**Do not open a public issue.** Instead, email the maintainer directly.
## Supported Versions
| Version | Supported |
|---------|-----------|
| Latest | Yes |
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate README exists
run: test -f README.md
- name: Validate LICENSE exists
run: test -f LICENSE
- name: Check markdown links
uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: "yes"
# Architecture Decision Record
{{ adr_content }}
---
name: Bug Report
about: Report a bug in this project
labels: bug
---
## Describe the Bug
A clear description of what the bug is.
## Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. See error
## Expected Behavior
What you expected to happen.
## Environment
- OS: [e.g. macOS, Windows]
- Version: [e.g. 1.0.0]
## What This PR Does
Brief description of the changes.
## How to Test
1. Step one
2. Step two
## Checklist
- [ ] README updated (if needed)
- [ ] Tests pass
- [ ] No new warnings
Your Copier template now generates completely different repo structures depending on the tier. An intern gets a clean, minimal repo. A senior gets ADRs, CI, issue templates, and phase-split tutorials. Next up, you will write the five subagent prompts that populate all of this content.
Write the 5 Helper Subagent Prompts
Your Copier template is built and ready to generate portfolio repos. The template slots (readme_content, adr_content, diagram_content, and others) are empty placeholders waiting for content.
Now you need the "brains" that fill those slots. In this step, you will write 5 specialized subagent prompts that Claude Code's Task tool will execute in parallel. Each prompt is a standalone markdown file with clear input, task, output, and rules sections.
In this step, get ready to:
- Write the structurer and credit-writer prompts that handle README content and NextWork attribution.
- Write the ADR-author and diagram-author prompts that generate decision records and Mermaid diagrams.
- Write the phase-splitter prompt that partitions walkthrough content into tutorial phases.
Write the structurer and credit-writer prompts
The structurer is the most important subagent. It takes the entire parsed NextWork doc and routes the content into a cognitive-funnel README layout. A cognitive funnel starts with the highest-level summary for recruiters, then drills into technical details for engineers.
- Create a new file at .claude/skills/forge-repo/prompts/structurer.md and paste the following content:
# Structurer Agent
You are a README content structurer for portfolio repos. You receive a parsed NextWork document as JSON.
## Input
A JSON object with fields: title, introducing, tools_and_concepts, project_reflection, walkthroughs.
## Task
Extract and restructure the content into README body sections following cognitive-funnel order:
1. **What This Project Does**: 2-3 sentences summarizing the project from the "introducing" field. Write for a recruiter who has 10 seconds.
2. **How It Works**: A brief technical summary drawn from the walkthroughs. 3-5 bullet points covering the key steps.
3. **Key Results**: What the project achieved or demonstrated, drawn from project_reflection. 2-3 bullet points.
## Output
Return ONLY the markdown content for these sections. No JSON wrapping. No extra headings. The parent will insert this into the README template.
## Rules
- Write in plain English at a 7th-grade reading level.
- Do not invent information that is not in the source document.
- Keep the total output under 300 words.
- Use bullet points over paragraphs where possible.
What does this prompt do?
The structurer takes the full parsed document and funnels it into three README sections. "What This Project Does" targets recruiters with a 10-second scan. "How It Works" gives hiring managers a technical summary. "Key Results" highlights outcomes from the project reflection.
The 300-word limit forces the subagent to distill, not dump. Bullet points keep the output scannable.
The credit-writer handles NextWork attribution. Every generated repo needs to properly credit the original project and author.
- Create a new file at .claude/skills/forge-repo/prompts/credit-writer.md and paste the following content:
# Credit Writer Agent
You format the NextWork source attribution for portfolio repos.
## Input
The title, author, and source_file fields from a parsed NextWork document.
## Task
Write a credit attribution block that:
1. Names the original NextWork project
2. Credits the author
3. Links to NextWork
4. Includes a brief "what I learned" sentence
## Output
Return a markdown block like:
---
**Built from NextWork**: This repo was created from the "[Project Title]" project on [NextWork](https://www.nextwork.org/). Original work by [Author].
## Rules
- Keep the credit block to 3-4 lines.
- Always include the NextWork link.
- Do not alter the project title or author name.
What does this prompt do?
The credit-writer is the simplest subagent. It takes just three fields (title, author, source file) and formats a short attribution block. The output feeds into both the nextwork-credit.md.jinja template and the README footer.
- Save both files.
- Confirm the files exist by running:
ls .claude/skills/forge-repo/prompts/
You should see structurer.md and credit-writer.md listed in the output.
Don't see the files?
Make sure you are running the ls command from the portfolioforge root folder. If you created the files in a different directory, move them into .claude/skills/forge-repo/prompts/.
Help me find where my prompt files ended up
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the complete structurer.md:
# Structurer Agent
You are a README content structurer for portfolio repos. You receive a parsed NextWork document as JSON.
## Input
A JSON object with fields: title, introducing, tools_and_concepts, project_reflection, walkthroughs.
## Task
Extract and restructure the content into README body sections following cognitive-funnel order:
1. **What This Project Does**: 2-3 sentences summarizing the project from the "introducing" field. Write for a recruiter who has 10 seconds.
2. **How It Works**: A brief technical summary drawn from the walkthroughs. 3-5 bullet points covering the key steps.
3. **Key Results**: What the project achieved or demonstrated, drawn from project_reflection. 2-3 bullet points.
## Output
Return ONLY the markdown content for these sections. No JSON wrapping. No extra headings. The parent will insert this into the README template.
## Rules
- Write in plain English at a 7th-grade reading level.
- Do not invent information that is not in the source document.
- Keep the total output under 300 words.
- Use bullet points over paragraphs where possible.
And here is the complete credit-writer.md:
# Credit Writer Agent
You format the NextWork source attribution for portfolio repos.
## Input
The title, author, and source_file fields from a parsed NextWork document.
## Task
Write a credit attribution block that:
1. Names the original NextWork project
2. Credits the author
3. Links to NextWork
4. Includes a brief "what I learned" sentence
## Output
Return a markdown block like:
---
**Built from NextWork**: This repo was created from the "[Project Title]" project on [NextWork](https://www.nextwork.org/). Original work by [Author].
## Rules
- Keep the credit block to 3-4 lines.
- Always include the NextWork link.
- Do not alter the project title or author name.
Write the ADR-author and diagram-author prompts
The next two subagents handle higher-tier content. The ADR-author drafts Architecture Decision Records in Nygard format. The diagram-author generates Mermaid system diagrams from the tools and concepts section.
What is an Architecture Decision Record?
An ADR is a short document that captures a technical decision: what was decided, why, and what the consequences are. The Nygard format uses five fields (Title, Status, Context, Decision, Consequences) to structure each record.
Recruiters and hiring managers look for ADRs in portfolio repos because they show you can reason about tradeoffs, not just write code.
- Create a new file at .claude/skills/forge-repo/prompts/adr-author.md and paste the following content:
# ADR Author Agent
You draft Architecture Decision Records in Nygard format from NextWork project reflection text.
## Input
The project_reflection text from a parsed NextWork document.
## Task
Infer 2-4 key technical decisions from the reflection text. For each, produce a Nygard-format ADR with:
- **Title**: A short decision statement starting with a verb (e.g., "Use Bedrock for LLM inference")
- **Status**: "Accepted"
- **Context**: What problem or requirement led to this decision (inferred from the reflection)
- **Decision**: What was chosen and why
- **Consequences**: What follows from this decision (positive and negative)
## Output
Return a JSON array of objects:
```json
[
{
"title": "Use Bedrock for LLM inference",
"status": "Accepted",
"context": "The project needed...",
"decision": "We chose...",
"consequences": "This means..."
}
]
```
## Rules
- Every claim must trace back to something in the source reflection text.
- Add this disclaimer to each ADR: "*This ADR was drafted by an AI helper from the project reflection. Review before merging.*"
- Do NOT invent tradeoffs the source text does not mention.
- If the reflection text is too thin to support an ADR, return fewer ADRs rather than hallucinating.
Why include an AI-drafted disclaimer?
The ADR prompt instructs the subagent to add a disclaimer to every record: "This ADR was drafted by an AI helper from the project reflection. Review before merging." This is honest about the origin of the content.
It also protects the learner. If the subagent infers a tradeoff that does not quite match reality, the disclaimer signals that the content needs a human review pass.
Next up is the diagram-author. This subagent reads the tools and concepts section and produces a Mermaid flowchart showing how the project's components connect.
- Create a new file at .claude/skills/forge-repo/prompts/diagram-author.md and paste the following content:
# Diagram Author Agent
You generate Mermaid system diagrams from NextWork project tool and concept descriptions.
## Input
The tools_and_concepts text from a parsed NextWork document.
## Task
Identify the named tools, services, and components mentioned in the text. Create a Mermaid flowchart diagram showing how they connect.
## Output
Return ONLY the Mermaid diagram code (without the ```mermaid fences). Example:
flowchart LR
A[User] --> B[API Gateway]
B --> C[Lambda Function]
C --> D[DynamoDB]
## Rules
- Use `flowchart LR` (left-to-right) layout.
- Use descriptive node labels, not abbreviations.
- Only include tools and services that are explicitly named in the source text.
- Keep the diagram to 15 nodes or fewer.
- Do not invent connections that the source text does not describe.
Why flowchart LR?
The prompt specifies flowchart LR (left-to-right) because architecture diagrams in READMEs read best horizontally. The 15-node cap keeps the diagram readable on a GitHub README without requiring scrolling.
- Save both files.
- Confirm all four prompt files exist so far by running:
ls .claude/skills/forge-repo/prompts/
You should see adr-author.md, credit-writer.md, diagram-author.md, and structurer.md listed.
Missing a file?
Double check the filenames match exactly. The orchestrator skill will look for these files by name. A typo like adr_author.md (underscore) instead of adr-author.md (hyphen) will cause the skill to fail later.
Help me check my prompt file names and locations
✔️ Awesome, I've got everything!
Great. Make sure all four files are saved before moving on to the final prompt.
ⓧ I'd like to double check the full code
Here is the complete adr-author.md:
# ADR Author Agent
You draft Architecture Decision Records in Nygard format from NextWork project reflection text.
## Input
The project_reflection text from a parsed NextWork document.
## Task
Infer 2-4 key technical decisions from the reflection text. For each, produce a Nygard-format ADR with:
- **Title**: A short decision statement starting with a verb (e.g., "Use Bedrock for LLM inference")
- **Status**: "Accepted"
- **Context**: What problem or requirement led to this decision (inferred from the reflection)
- **Decision**: What was chosen and why
- **Consequences**: What follows from this decision (positive and negative)
## Output
Return a JSON array of objects:
```json
[
{
"title": "Use Bedrock for LLM inference",
"status": "Accepted",
"context": "The project needed...",
"decision": "We chose...",
"consequences": "This means..."
}
]
```
## Rules
- Every claim must trace back to something in the source reflection text.
- Add this disclaimer to each ADR: "*This ADR was drafted by an AI helper from the project reflection. Review before merging.*"
- Do NOT invent tradeoffs the source text does not mention.
- If the reflection text is too thin to support an ADR, return fewer ADRs rather than hallucinating.
And here is the complete diagram-author.md:
# Diagram Author Agent
You generate Mermaid system diagrams from NextWork project tool and concept descriptions.
## Input
The tools_and_concepts text from a parsed NextWork document.
## Task
Identify the named tools, services, and components mentioned in the text. Create a Mermaid flowchart diagram showing how they connect.
## Output
Return ONLY the Mermaid diagram code (without the ```mermaid fences). Example:
flowchart LR
A[User] --> B[API Gateway]
B --> C[Lambda Function]
C --> D[DynamoDB]
## Rules
- Use `flowchart LR` (left-to-right) layout.
- Use descriptive node labels, not abbreviations.
- Only include tools and services that are explicitly named in the source text.
- Keep the diagram to 15 nodes or fewer.
- Do not invent connections that the source text does not describe.
Write the phase-splitter prompt
The phase-splitter only runs for mid and senior tier projects. It takes the array of walkthrough sections and groups them into logical phases (P01 through P04) for the docs/tutorials/ directory you created in the template.
- Create a new file at .claude/skills/forge-repo/prompts/phase-splitter.md and paste the following content:
# Phase Splitter Agent
You partition sequential NextWork walkthrough content into phases for senior-tier documentation.
## Input
A JSON array of walkthrough strings from a parsed NextWork document chain.
## Task
Group the walkthroughs into 2-4 phases (P01, P02, P03, P04) based on logical project stages:
- P01: Setup and foundation
- P02: Core implementation
- P03: Integration and testing
- P04: Polish and deployment
## Output
Return a JSON array of phase objects:
```json
[
{
"phase_id": "P01",
"title": "Foundation and Setup",
"content": "The merged walkthrough content for this phase..."
}
]
```
## Rules
- Preserve all code blocks and command examples from the original walkthroughs.
- Each phase should contain at least one walkthrough section.
- Add a brief 1-2 sentence summary at the start of each phase.
- If there are fewer than 4 walkthroughs, use fewer phases. Minimum 2 phases.
What does this prompt do?
The phase-splitter groups walkthrough content into logical project stages. P01 covers setup and foundation. P02 handles core implementation. P03 is integration and testing. P04 covers polish and deployment.
The minimum of 2 phases prevents the subagent from collapsing everything into a single document. The rule to preserve code blocks ensures tutorial content stays usable.
- Save phase-splitter.md.
- Confirm all five prompt files are in place by running:
ls .claude/skills/forge-repo/prompts/
You should see all five files: adr-author.md, credit-writer.md, diagram-author.md, phase-splitter.md, and structurer.md.
Only see 4 files?
Check that phase-splitter.md was saved in the .claude/skills/forge-repo/prompts/ directory, not in a parent folder. Unsaved files in your editor also will not appear in the ls output.
Help me find my missing prompt file
✔️ Awesome, I've got everything!
Great. Double check you have saved all five files.
ⓧ I'd like to double check the full code
Here is the complete phase-splitter.md:
# Phase Splitter Agent
You partition sequential NextWork walkthrough content into phases for senior-tier documentation.
## Input
A JSON array of walkthrough strings from a parsed NextWork document chain.
## Task
Group the walkthroughs into 2-4 phases (P01, P02, P03, P04) based on logical project stages:
- P01: Setup and foundation
- P02: Core implementation
- P03: Integration and testing
- P04: Polish and deployment
## Output
Return a JSON array of phase objects:
```json
[
{
"phase_id": "P01",
"title": "Foundation and Setup",
"content": "The merged walkthrough content for this phase..."
}
]
```
## Rules
- Preserve all code blocks and command examples from the original walkthroughs.
- Each phase should contain at least one walkthrough section.
- Add a brief 1-2 sentence summary at the start of each phase.
- If there are fewer than 4 walkthroughs, use fewer phases. Minimum 2 phases.
How do all 5 prompts work together?
Each subagent has a single, focused job. The structurer fills the README body. The credit-writer handles attribution. The ADR-author drafts decision records. The diagram-author creates a visual architecture map. The phase-splitter organizes walkthroughs into tutorial phases.
Because they are all independent (none reads another's output), Claude Code can run all five simultaneously using the Task tool. This is what makes the forge fast.
All five subagent prompts are in place. Next up, you will build the orchestrator skill that ties everything together. The SKILL.md file will read the doc, detect the tier, launch these five prompts in parallel, and assemble the final repo.
Build the /forge-repo Orchestrator Skill and Conformance Checker
You now have every building block for PortfolioForge: a doc parser, a tier detector, a full Copier template, and five subagent prompts. The missing piece is the orchestrator that wires them all together into a single command.
In this step, you'll write the /forge-repo Claude Code skill that coordinates the entire pipeline. You'll also build a conformance checker that scores generated repos against a 16-axis rubric to guarantee quality.
In this step, get ready to:
- Write the /forge-repo skill file with parallel subagent orchestration.
- Build a 16-axis conformance checker with tier-based scoring targets.
- Create a CLAUDE.md project memory file for consistent behavior.
Write the orchestrator skill definition
A Claude Code skill is a markdown file that tells Claude how to respond when you invoke a slash command. The SKILL.md file lives in the .claude/skills/forge-repo/ directory and contains YAML frontmatter (declaring the skill's name and allowed tools) plus detailed orchestration instructions.
- Create a file called SKILL.md inside .claude/skills/forge-repo/ and paste the following content:
---
name: forge-repo
description: "Convert a NextWork project doc into a portfolio-ready GitHub repo. Use when the user wants to generate a repo from a NextWork document."
allowed-tools:
- Read
- Write
- Bash
- Glob
- Grep
- Task
---
# PortfolioForge: NextWork Doc to Portfolio Repo
Convert a NextWork project doc into a 100%-signal GitHub portfolio repo.
## Usage
`/forge-repo <doc-path> [--tier intern|mid|senior] [--target <owner>/<repo>]`
- `<doc-path>`: Path to a NextWork markdown file OR a directory of sequential files.
- `--tier`: Override auto-detected tier. Optional.
- `--target`: GitHub repo to open the PR against. Optional.
## Orchestration Steps
When invoked, follow these steps exactly:
### 1. Parse Arguments
Parse `$ARGUMENTS` to extract:
- `doc_path` (required, first positional argument)
- `tier_override` (optional, from --tier flag)
- `target_repo` (optional, from --target flag)
### 2. Read and Parse the NextWork Doc
Run the parser:
```bash
python src/nextwork_doc.py "<doc_path>"
```
Capture the JSON output. This gives you the structured document fields.
### 3. Detect Tier
If no --tier override was provided, run:
```bash
python src/tier_detector.py "<doc_path>"
```
Use the output as the tier. Otherwise use the override value.
### 4. Launch 5 Parallel Subagents
Launch ALL FIVE of these subagents simultaneously using the Task tool. They are independent and can run in parallel. Do NOT run them sequentially.
**Subagent 1: Structurer**
Read the prompt from `.claude/skills/forge-repo/prompts/structurer.md`. Pass it the parsed doc JSON. It returns the README body content as a string.
**Subagent 2: ADR Author**
Read the prompt from `.claude/skills/forge-repo/prompts/adr-author.md`. Pass it the project_reflection field. It returns ADR content as a JSON array of objects with title, status, context, decision, and consequences fields.
**Subagent 3: Diagram Author**
Read the prompt from `.claude/skills/forge-repo/prompts/diagram-author.md`. Pass it the tools_and_concepts field. It returns Mermaid diagram code as a string.
**Subagent 4: Phase Splitter**
Read the prompt from `.claude/skills/forge-repo/prompts/phase-splitter.md`. Pass it the walkthroughs array. It returns phase-split content as a JSON array. Only relevant for mid/senior tier. For intern tier, skip this agent.
**Subagent 5: Credit Writer**
Read the prompt from `.claude/skills/forge-repo/prompts/credit-writer.md`. Pass it the title, author, and source_file fields. It returns the formatted NextWork credit block as a string.
What does this skill file do?
The YAML frontmatter at the top registers this as a Claude Code skill named forge-repo. The allowed-tools list grants the skill permission to read files, write files, run shell commands, search files, and most importantly spawn subagents via the Task tool.
Steps 1 through 3 handle setup: parsing the user's command-line arguments, running your nextwork_doc.py parser, and detecting the tier. Step 4 is where the parallel magic happens. Claude Code reads each subagent prompt file, fans out five independent tasks simultaneously, and collects their results.
Why parallel subagents instead of sequential?
Each subagent works on a different slice of the parsed document. The structurer reads the introduction, the ADR author reads the reflection, and the diagram author reads the tools section. None of them depend on each other's output.
Running them in parallel via Claude Code's Task tool cuts wall-clock time from minutes to seconds. The explicit "do NOT run sequentially" instruction is critical because Claude Code defaults to conservative sequential execution without it.
- Save SKILL.md.
You won't see a visible change yet. The next two chunks add the assembly and quality-check sections that complete the pipeline.
File not saving in the right place?
Make sure the file is at .claude/skills/forge-repo/SKILL.md and not inside the prompts/ subdirectory. The five prompt files live in prompts/, but SKILL.md sits one level up, directly inside forge-repo/.
Help me fix my SKILL.md file location in the PortfolioForge project.
Add template assembly and PR creation to the skill
Once all five subagents return their results, the skill needs to assemble the data and feed it into your Copier template. It also needs to initialize a Git repo and push the result.
- In SKILL.md, add the following two sections at the end of the file, after step 4:
### 5. Assemble and Generate
Once all subagents return, run Copier with the assembled data:
```bash
copier copy --data "project_name=<title>" --data "project_slug=<slug>" --data "description=<description>" --data "tier=<tier>" --data "author=<author>" --data "readme_content=<structurer_output>" --data "adr_content=<adr_output>" --data "diagram_content=<diagram_output>" --data "credit_content=<credit_output>" --data "phase_content=<phase_output>" --data "tools_used=<tools>" --data "nextwork_source=<source_file>" --defaults --vcs-ref HEAD templates/portfolio-repo/ ./output/<project_slug>/
```
### 6. Conformance Check
Run the checker:
```bash
python src/conformance_checker.py "./output/<project_slug>/" "<tier>"
```
Parse the JSON output. If `meets_target` is false, look at the `failures` list. For each failure, regenerate the missing content by re-running the relevant subagent or creating the missing file directly. Then re-run the checker. Retry up to 2 times.
### 7. Open PR
If a --target repo was specified:
```bash
cd "./output/<project_slug>/"
git init
git add -A
git commit -m "feat: add <project_name> portfolio repo (forged from NextWork)"
gh repo create <target> --public --source . --push
```
Report the final conformance score and the repo URL.
What do these sections do?
- Step 5 (Assemble and Generate) takes all five subagent outputs and passes them to Copier via --data flags. The --defaults flag skips interactive prompts. The --vcs-ref HEAD flag uses your local template including uncommitted changes.
- Step 6 (Conformance Check) runs the checker you'll build next. If the score falls below the tier target, the skill retries up to 2 times by regenerating only the failing axes.
- Step 7 (Open PR) initializes a Git repo in the output, commits with a Conventional Commits message, and uses gh repo create to push it to GitHub in one command.
- Save SKILL.md.
- Scroll through the file and confirm it has 7 numbered orchestration steps, from Parse Arguments through Open PR.
Steps not numbered 1-7?
Make sure you pasted the new sections after the end of step 4 (the last subagent description). The final order should be: 1. Parse Arguments, 2. Read and Parse, 3. Detect Tier, 4. Launch Subagents, 5. Assemble and Generate, 6. Conformance Check, 7. Open PR.
Help me check my SKILL.md structure.
✔️ Awesome, I've got everything!
Great. Make sure you've saved the file before moving on.
ⓧ I'd like to double check the full code
Here is the complete SKILL.md file. Compare it against yours:
---
name: forge-repo
description: "Convert a NextWork project doc into a portfolio-ready GitHub repo. Use when the user wants to generate a repo from a NextWork document."
allowed-tools:
- Read
- Write
- Bash
- Glob
- Grep
- Task
---
# PortfolioForge: NextWork Doc to Portfolio Repo
Convert a NextWork project doc into a 100%-signal GitHub portfolio repo.
## Usage
`/forge-repo <doc-path> [--tier intern|mid|senior] [--target <owner>/<repo>]`
- `<doc-path>`: Path to a NextWork markdown file OR a directory of sequential files.
- `--tier`: Override auto-detected tier. Optional.
- `--target`: GitHub repo to open the PR against. Optional.
## Orchestration Steps
When invoked, follow these steps exactly:
### 1. Parse Arguments
Parse `$ARGUMENTS` to extract:
- `doc_path` (required, first positional argument)
- `tier_override` (optional, from --tier flag)
- `target_repo` (optional, from --target flag)
### 2. Read and Parse the NextWork Doc
Run the parser:
```bash
python src/nextwork_doc.py "<doc_path>"
```
Capture the JSON output. This gives you the structured document fields.
### 3. Detect Tier
If no --tier override was provided, run:
```bash
python src/tier_detector.py "<doc_path>"
```
Use the output as the tier. Otherwise use the override value.
### 4. Launch 5 Parallel Subagents
Launch ALL FIVE of these subagents simultaneously using the Task tool. They are independent and can run in parallel. Do NOT run them sequentially.
**Subagent 1: Structurer**
Read the prompt from `.claude/skills/forge-repo/prompts/structurer.md`. Pass it the parsed doc JSON. It returns the README body content as a string.
**Subagent 2: ADR Author**
Read the prompt from `.claude/skills/forge-repo/prompts/adr-author.md`. Pass it the project_reflection field. It returns ADR content as a JSON array of objects with title, status, context, decision, and consequences fields.
**Subagent 3: Diagram Author**
Read the prompt from `.claude/skills/forge-repo/prompts/diagram-author.md`. Pass it the tools_and_concepts field. It returns Mermaid diagram code as a string.
**Subagent 4: Phase Splitter**
Read the prompt from `.claude/skills/forge-repo/prompts/phase-splitter.md`. Pass it the walkthroughs array. It returns phase-split content as a JSON array. Only relevant for mid/senior tier. For intern tier, skip this agent.
**Subagent 5: Credit Writer**
Read the prompt from `.claude/skills/forge-repo/prompts/credit-writer.md`. Pass it the title, author, and source_file fields. It returns the formatted NextWork credit block as a string.
### 5. Assemble and Generate
Once all subagents return, run Copier with the assembled data:
```bash
copier copy --data "project_name=<title>" --data "project_slug=<slug>" --data "description=<description>" --data "tier=<tier>" --data "author=<author>" --data "readme_content=<structurer_output>" --data "adr_content=<adr_output>" --data "diagram_content=<diagram_output>" --data "credit_content=<credit_output>" --data "phase_content=<phase_output>" --data "tools_used=<tools>" --data "nextwork_source=<source_file>" --defaults --vcs-ref HEAD templates/portfolio-repo/ ./output/<project_slug>/
```
### 6. Conformance Check
Run the checker:
```bash
python src/conformance_checker.py "./output/<project_slug>/" "<tier>"
```
Parse the JSON output. If `meets_target` is false, look at the `failures` list. For each failure, regenerate the missing content by re-running the relevant subagent or creating the missing file directly. Then re-run the checker. Retry up to 2 times.
### 7. Open PR
If a --target repo was specified:
```bash
cd "./output/<project_slug>/"
git init
git add -A
git commit -m "feat: add <project_name> portfolio repo (forged from NextWork)"
gh repo create <target> --public --source . --push
```
Report the final conformance score and the repo URL.
Create the conformance checker
The conformance checker is a Python script that scores a generated repo against 16 rubric axes. Each axis checks for the existence of a specific file or content pattern. Axes are filtered by tier so intern repos are only checked against intern-level requirements.
- Create a new file called conformance_checker.py inside your src/ directory and paste the following imports and configuration:
"""Conformance checker for generated portfolio repos. Scores against 16 rubric axes."""
import json
import sys
from pathlib import Path
# Each axis defines a quality signal to check in the generated repo
RUBRIC_AXES = [
{"id": 1, "name": "README exists", "path": "README.md", "min_tier": "intern"},
{"id": 2, "name": "README has title", "check": "readme_title", "min_tier": "intern"},
{"id": 3, "name": "README has description", "check": "readme_description", "min_tier": "intern"},
{"id": 4, "name": "README has badges", "check": "readme_badges", "min_tier": "intern"},
{"id": 5, "name": "LICENSE exists", "path": "LICENSE", "min_tier": "intern"},
{"id": 6, "name": "CONTRIBUTING exists", "path": "CONTRIBUTING.md", "min_tier": "intern"},
{"id": 7, "name": "NextWork credit present", "path": "nextwork-credit.md", "min_tier": "intern"},
{"id": 8, "name": ".gitignore exists", "path": ".gitignore", "min_tier": "intern"},
{"id": 9, "name": "CODE_OF_CONDUCT exists", "path": "CODE_OF_CONDUCT.md", "min_tier": "mid"},
{"id": 10, "name": "SECURITY exists", "path": "SECURITY.md", "min_tier": "mid"},
{"id": 11, "name": "ADR files exist", "check": "adr_exists", "min_tier": "mid"},
{"id": 12, "name": "Mermaid diagram exists", "check": "mermaid_exists", "min_tier": "mid"},
{"id": 13, "name": "CI workflow exists", "path": ".github/workflows/ci.yml", "min_tier": "mid"},
{"id": 14, "name": "Tutorials exist", "check": "tutorials_exist", "min_tier": "mid"},
{"id": 15, "name": "Phase-split docs exist", "check": "phases_exist", "min_tier": "senior"},
{"id": 16, "name": "ISSUE_TEMPLATE exists", "check": "issue_template_exists", "min_tier": "senior"},
]
# Maps tier names to numeric levels for comparison
TIER_ORDER = {"intern": 0, "mid": 1, "senior": 2}
# Minimum passing score percentage for each tier
TIER_TARGETS = {"intern": 75, "mid": 90, "senior": 95}
What does this configuration do?
The RUBRIC_AXES list defines 16 quality signals. Some use a simple file path check (does the file exist?). Others use a check key that points to a custom function for content-level validation.
The min_tier field on each axis controls which tiers it applies to. An intern repo is only checked against the 8 intern-level axes. A senior repo is checked against all 16.
- Save conformance_checker.py.
You won't see a change yet. The next chunk adds the functions that use this configuration.
- Below the TIER_TARGETS dictionary, add the two helper functions that check individual axes:
def check_readme_content(repo_path: Path, check_type: str) -> bool:
"""Check specific README content requirements."""
readme = repo_path / "README.md"
if not readme.exists():
return False
content = readme.read_text(encoding="utf-8")
if check_type == "readme_title":
return content.strip().startswith("#")
elif check_type == "readme_description":
lines = [line for line in content.split("\n") if line.strip() and not line.startswith("#")]
return len(lines) >= 1
elif check_type == "readme_badges":
return "![" in content or "[![" in content
return False
def check_special(repo_path: Path, check_type: str) -> bool:
"""Check non-file-path rubric axes."""
if check_type.startswith("readme_"):
return check_readme_content(repo_path, check_type)
elif check_type == "adr_exists":
decisions_dir = repo_path / "docs" / "decisions"
if not decisions_dir.exists():
return False
return any(decisions_dir.glob("*.md"))
elif check_type == "mermaid_exists":
# Search all markdown files for a mermaid code block
for md_file in repo_path.rglob("*.md"):
content = md_file.read_text(encoding="utf-8")
if "```mermaid" in content:
return True
return False
elif check_type == "tutorials_exist":
tutorials_dir = repo_path / "docs" / "tutorials"
return tutorials_dir.exists() and any(tutorials_dir.glob("*.md"))
elif check_type == "phases_exist":
tutorials_dir = repo_path / "docs" / "tutorials"
if not tutorials_dir.exists():
return False
# Phase files start with P0 prefix (P01, P02, etc.)
phase_files = [f for f in tutorials_dir.glob("*.md") if f.stem.startswith("P0")]
return len(phase_files) >= 2
elif check_type == "issue_template_exists":
template_dir = repo_path / ".github" / "ISSUE_TEMPLATE"
return template_dir.exists() and any(template_dir.glob("*.md"))
return False
What do these functions check?
- check_readme_content() handles the three README content axes: does it start with a heading (title), have at least one non-heading line (description), and contain badge markdown syntax.
- check_special() routes all non-file-path checks. It delegates README checks to the first function, then handles ADR directories, Mermaid code blocks, tutorial directories, phase-split files, and issue templates.
- Save conformance_checker.py.
You won't see a change yet. The next chunk adds the main scoring function that ties everything together.
- Below the check_special() function, add the main scoring function and command-line entry point:
def check_conformance(repo_path: str, tier: str) -> dict:
"""Score a generated repo against the 16-axis rubric for the given tier."""
path = Path(repo_path)
tier_level = TIER_ORDER.get(tier, 0)
results = []
passed = 0
applicable = 0
for axis in RUBRIC_AXES:
axis_tier_level = TIER_ORDER.get(axis["min_tier"], 0)
# Skip axes that are above the current tier level
if axis_tier_level > tier_level:
continue
applicable += 1
# Check by file path or by custom check function
if "path" in axis:
ok = (path / axis["path"]).exists()
elif "check" in axis:
ok = check_special(path, axis["check"])
else:
ok = False
if ok:
passed += 1
results.append({"axis": axis["id"], "name": axis["name"], "passed": ok})
# Calculate percentage score
score = round((passed / applicable) * 100) if applicable > 0 else 0
target = TIER_TARGETS.get(tier, 75)
return {
"tier": tier,
"score": score,
"target": target,
"passed": passed,
"applicable": applicable,
"meets_target": score >= target,
"failures": [r["name"] for r in results if not r["passed"]],
"details": results,
}
if __name__ == "__main__":
repo_path = sys.argv[1]
tier = sys.argv[2] if len(sys.argv) > 2 else "intern"
report = check_conformance(repo_path, tier)
print(json.dumps(report, indent=2))
What does check_conformance() return?
The function loops through all 16 rubric axes, skipping any whose min_tier is above the current tier. For each applicable axis, it checks for the file or runs the custom check function.
The return dictionary includes the percentage score, the tier target (75% for intern, 90% for mid, 95% for senior), a boolean meets_target flag, and a list of failures for the skill's retry logic to act on.
- Save conformance_checker.py.
- Verify the script loads without errors by running:
python src/conformance_checker.py . intern
You should see a JSON report with "score": 0 and all axes listed as failures. That is expected because your current directory is not a generated portfolio repo.
Seeing a ModuleNotFoundError or SyntaxError?
Check that you saved all three sections to the same file. A common mistake is pasting the helper functions into a new file instead of appending them below the constants in conformance_checker.py.
Also confirm you're running the command from the portfolioforge/ directory, not from inside src/.
Help me debug my conformance_checker.py script.
✔️ Awesome, I've got everything!
Great. Double check you've saved the file before moving on.
ⓧ I'd like to double check the full code
Here is the complete src/conformance_checker.py file:
"""Conformance checker for generated portfolio repos. Scores against 16 rubric axes."""
import json
import sys
from pathlib import Path
RUBRIC_AXES = [
{"id": 1, "name": "README exists", "path": "README.md", "min_tier": "intern"},
{"id": 2, "name": "README has title", "check": "readme_title", "min_tier": "intern"},
{"id": 3, "name": "README has description", "check": "readme_description", "min_tier": "intern"},
{"id": 4, "name": "README has badges", "check": "readme_badges", "min_tier": "intern"},
{"id": 5, "name": "LICENSE exists", "path": "LICENSE", "min_tier": "intern"},
{"id": 6, "name": "CONTRIBUTING exists", "path": "CONTRIBUTING.md", "min_tier": "intern"},
{"id": 7, "name": "NextWork credit present", "path": "nextwork-credit.md", "min_tier": "intern"},
{"id": 8, "name": ".gitignore exists", "path": ".gitignore", "min_tier": "intern"},
{"id": 9, "name": "CODE_OF_CONDUCT exists", "path": "CODE_OF_CONDUCT.md", "min_tier": "mid"},
{"id": 10, "name": "SECURITY exists", "path": "SECURITY.md", "min_tier": "mid"},
{"id": 11, "name": "ADR files exist", "check": "adr_exists", "min_tier": "mid"},
{"id": 12, "name": "Mermaid diagram exists", "check": "mermaid_exists", "min_tier": "mid"},
{"id": 13, "name": "CI workflow exists", "path": ".github/workflows/ci.yml", "min_tier": "mid"},
{"id": 14, "name": "Tutorials exist", "check": "tutorials_exist", "min_tier": "mid"},
{"id": 15, "name": "Phase-split docs exist", "check": "phases_exist", "min_tier": "senior"},
{"id": 16, "name": "ISSUE_TEMPLATE exists", "check": "issue_template_exists", "min_tier": "senior"},
]
TIER_ORDER = {"intern": 0, "mid": 1, "senior": 2}
TIER_TARGETS = {"intern": 75, "mid": 90, "senior": 95}
def check_readme_content(repo_path: Path, check_type: str) -> bool:
"""Check specific README content requirements."""
readme = repo_path / "README.md"
if not readme.exists():
return False
content = readme.read_text(encoding="utf-8")
if check_type == "readme_title":
return content.strip().startswith("#")
elif check_type == "readme_description":
lines = [line for line in content.split("\n") if line.strip() and not line.startswith("#")]
return len(lines) >= 1
elif check_type == "readme_badges":
return "![" in content or "[![" in content
return False
def check_special(repo_path: Path, check_type: str) -> bool:
"""Check non-file-path rubric axes."""
if check_type.startswith("readme_"):
return check_readme_content(repo_path, check_type)
elif check_type == "adr_exists":
decisions_dir = repo_path / "docs" / "decisions"
if not decisions_dir.exists():
return False
return any(decisions_dir.glob("*.md"))
elif check_type == "mermaid_exists":
for md_file in repo_path.rglob("*.md"):
content = md_file.read_text(encoding="utf-8")
if "```mermaid" in content:
return True
return False
elif check_type == "tutorials_exist":
tutorials_dir = repo_path / "docs" / "tutorials"
return tutorials_dir.exists() and any(tutorials_dir.glob("*.md"))
elif check_type == "phases_exist":
tutorials_dir = repo_path / "docs" / "tutorials"
if not tutorials_dir.exists():
return False
phase_files = [f for f in tutorials_dir.glob("*.md") if f.stem.startswith("P0")]
return len(phase_files) >= 2
elif check_type == "issue_template_exists":
template_dir = repo_path / ".github" / "ISSUE_TEMPLATE"
return template_dir.exists() and any(template_dir.glob("*.md"))
return False
def check_conformance(repo_path: str, tier: str) -> dict:
"""Score a generated repo against the 16-axis rubric for the given tier."""
path = Path(repo_path)
tier_level = TIER_ORDER.get(tier, 0)
results = []
passed = 0
applicable = 0
for axis in RUBRIC_AXES:
axis_tier_level = TIER_ORDER.get(axis["min_tier"], 0)
if axis_tier_level > tier_level:
continue
applicable += 1
if "path" in axis:
ok = (path / axis["path"]).exists()
elif "check" in axis:
ok = check_special(path, axis["check"])
else:
ok = False
if ok:
passed += 1
results.append({"axis": axis["id"], "name": axis["name"], "passed": ok})
score = round((passed / applicable) * 100) if applicable > 0 else 0
target = TIER_TARGETS.get(tier, 75)
return {
"tier": tier,
"score": score,
"target": target,
"passed": passed,
"applicable": applicable,
"meets_target": score >= target,
"failures": [r["name"] for r in results if not r["passed"]],
"details": results,
}
if __name__ == "__main__":
repo_path = sys.argv[1]
tier = sys.argv[2] if len(sys.argv) > 2 else "intern"
report = check_conformance(repo_path, tier)
print(json.dumps(report, indent=2))
The last piece is a CLAUDE.md file at the project root. This acts as a project memory file that Claude Code reads automatically whenever it opens your project, keeping it oriented across sessions.
- Create a file called CLAUDE.md at the root of your portfolioforge/ folder and paste the following:
# PortfolioForge
This project is a Claude Code skill that converts NextWork project docs into portfolio-ready GitHub repos.
## Project Structure
- `src/` - Python utilities (doc parser, tier detector, conformance checker)
- `templates/portfolio-repo/` - Copier template with tier-conditional files
- `.claude/skills/forge-repo/` - The /forge-repo skill and subagent prompts
## Key Conventions
- All Python code uses Pydantic for data validation.
- The Copier template uses Jinja2 conditionals for tier-based file generation.
- Subagent prompts are in `.claude/skills/forge-repo/prompts/`.
- The conformance checker scores repos against 16 axes.
## Tier Definitions
- **intern**: Single NextWork doc, basic repo (README, LICENSE, credit)
- **mid**: 2-4 sequential docs, full repo (adds ADRs, diagram, CI, community files)
- **senior**: 5+ sequential docs, engineering-grade repo (adds phase-split docs, issue templates)
## When Running /forge-repo
Always spawn the 5 helper subagents in parallel using the Task tool. Do not run them sequentially. The structurer, ADR-author, diagram-author, phase-splitter, and credit-writer are independent and produce separate outputs that get assembled after all complete.
Why a CLAUDE.md file?
Claude Code automatically reads CLAUDE.md when it opens a project. This file gives Claude persistent context about your project structure, naming conventions, and tier definitions without you having to explain them every session.
The parallel subagent reminder at the bottom is especially important. Without it, Claude Code tends to run subagents one at a time instead of simultaneously.
- Save CLAUDE.md.
- Verify your project now has the three new files by running:
ls CLAUDE.md src/conformance_checker.py .claude/skills/forge-repo/SKILL.md
You should see all three file paths listed in the output with no errors.
One of the files is missing?
If ls reports "No such file or directory" for any of the three paths, make sure you created the file in the correct location. CLAUDE.md lives at the project root. conformance_checker.py lives inside src/. SKILL.md lives inside .claude/skills/forge-repo/.
Help me find my missing PortfolioForge files.
✔️ Awesome, I've got everything!
Great. Make sure all files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the complete CLAUDE.md file:
# PortfolioForge
This project is a Claude Code skill that converts NextWork project docs into portfolio-ready GitHub repos.
## Project Structure
- `src/` - Python utilities (doc parser, tier detector, conformance checker)
- `templates/portfolio-repo/` - Copier template with tier-conditional files
- `.claude/skills/forge-repo/` - The /forge-repo skill and subagent prompts
## Key Conventions
- All Python code uses Pydantic for data validation.
- The Copier template uses Jinja2 conditionals for tier-based file generation.
- Subagent prompts are in `.claude/skills/forge-repo/prompts/`.
- The conformance checker scores repos against 16 axes.
## Tier Definitions
- **intern**: Single NextWork doc, basic repo (README, LICENSE, credit)
- **mid**: 2-4 sequential docs, full repo (adds ADRs, diagram, CI, community files)
- **senior**: 5+ sequential docs, engineering-grade repo (adds phase-split docs, issue templates)
## When Running /forge-repo
Always spawn the 5 helper subagents in parallel using the Task tool. Do not run them sequentially. The structurer, ADR-author, diagram-author, phase-splitter, and credit-writer are independent and produce separate outputs that get assembled after all complete.
Your entire pipeline is now defined: a skill file that orchestrates 5 parallel subagents, a conformance checker that enforces quality, and a project memory file that keeps Claude oriented. Next up, you'll run the full pipeline end-to-end on a real NextWork doc and see PortfolioForge in action.
Run Your First End-to-End Forge
You have a complete pipeline sitting in your portfolioforge/ directory. A Pydantic doc parser, an auto-tier detector, a Copier template with conditional file generation, 5 subagent prompts, a SKILL.md orchestrator, and a 16-axis conformance checker.
Now it is time to light the fuse. In this step, you will feed a sample NextWork doc into the /forge-repo skill and watch it parse, fan out subagents, assemble a repo through Copier, and produce a portfolio-ready output. Then you will verify the result against the conformance rubric.
In this step, get ready to:
- Create a sample NextWork doc and run the full forge pipeline end-to-end.
- Run the conformance checker and verify the output meets the intern-tier target.
Create a sample doc and run the forge
The /forge-repo skill expects a NextWork project doc with 4 standard sections. You need a test file to feed it.
- Make sure your terminal is inside the portfolioforge/ directory.
- Create the test-docs directory by running this command:
mkdir test-docs
- Confirm the directory exists by running:
ls
You should see test-docs listed alongside src/, templates/, and your other project folders.
- Create a file called sample-project.md inside the test-docs/ directory and paste the following content into it:
# Build a Cloud Resume
## Introducing Today's Project
In this project, I built a cloud-hosted resume website using Amazon S3 and CloudFront. The goal was to learn how to deploy static websites to the cloud and serve them through a content delivery network.
## Tools and Concepts
- **Amazon S3**: Object storage service used to host the static website files.
- **Amazon CloudFront**: CDN that serves the website with low latency globally.
- **HTML/CSS**: Used to build the resume page layout and styling.
- **AWS CLI**: Command-line tool for uploading files to S3.
## Project Reflection
I chose S3 for static hosting because it removes the need to manage a web server. This was simpler than setting up an EC2 instance with Nginx. CloudFront was added to enable HTTPS and improve load times for users around the world. One challenge was configuring the bucket policy to allow public reads while restricting writes to only authenticated users.
## Step 1: Set Up the S3 Bucket
I created a new S3 bucket in us-east-1 with static website hosting enabled. I set index.html as the default document and configured the bucket policy for public read access.
## Step 2: Build and Upload the Resume
I wrote the HTML and CSS for my resume page locally. Then I used the AWS CLI to sync my local files to the bucket with the aws s3 sync command.
## Step 3: Add CloudFront Distribution
I created a CloudFront distribution pointing to the S3 bucket origin. I set the default root object to index.html and waited for the distribution to finish deploying.
What is in this sample doc?
This sample contains the 4 standard sections that the doc parser expects:
- Introducing Today's Project gives the structurer agent the hook and summary for the README.
- Tools and Concepts feeds the diagram agent the named services it maps into Mermaid nodes.
- Project Reflection gives the ADR agent material to infer architecture decisions from.
- Step walkthroughs provide the phase splitter with content to partition (used for mid/senior tiers).
- Save sample-project.md.
- Confirm the file exists by running:
ls test-docs/
You should see sample-project.md listed in the output.
File not showing up?
- Make sure you saved the file inside the test-docs/ directory, not in the root portfolioforge/ folder.
- Check that the filename is exactly sample-project.md with the .md extension.
Why isn't my sample-project.md file appearing in the test-docs directory?
Now it is time to fire up Claude Code and let the skill do its work. Since this is a single loose file, the tier detector will classify it as intern tier automatically.
- Open Claude Code in the portfolioforge/ directory by running this command in your terminal:
claude
Already have Claude Code open?
If you already have Claude Code running in this directory from a previous step, you can skip the claude command and use your existing session. Just make sure the working directory shown in Claude Code is your portfolioforge/ folder.
- Once Claude Code is ready, invoke the forge skill by typing the following at the Claude Code prompt and pressing Enter:
/forge-repo test-docs/sample-project.md
What should I see?
Claude Code reads your SKILL.md and follows its orchestration steps. Watch the output carefully. You should see it:
- Run nextwork_doc.py to parse the sample doc into structured JSON.
- Run tier_detector.py and detect the tier as intern (single file).
- Spawn subagents via the Task tool. For intern tier, it launches 4 agents (the phase splitter is skipped).
- Assemble the outputs and run copier copy to generate the repo.
- Run the conformance checker and report the score.
The full process typically takes 30 to 90 seconds depending on how quickly the subagents complete.
Claude Code may ask for permissions
Claude Code might prompt you to approve tool usage (Read, Write, Bash, Task) when the skill runs. Accept these permissions so the skill can execute its full pipeline.
- Once Claude Code finishes, verify the generated repo files by running this in your terminal:
ls ./output/build-a-cloud-resume/
You should see README.md, LICENSE, CONTRIBUTING.md, nextwork-credit.md, .gitignore, and a Copier answers file. These are the 6 base files that every intern-tier repo gets.
Why only these files?
The intern tier produces the simplest repo: a README, license, contributing guide, NextWork credit, and gitignore. Community-profile files like CODE_OF_CONDUCT.md and SECURITY.md only appear for mid and senior tiers.
This is your Copier template's conditional file generation at work. The {% if tier != 'intern' %} syntax in those template filenames prevented them from being created.
Output directory is empty or missing?
- Check Claude Code's output for error messages. The most common issue is a Python import error if Pydantic is not installed in your current environment.
- Verify that copier is installed by running copier --version in your terminal. If it is not found, run pip install copier==9.14.1.
- If Claude Code ran the skill but the output path is different, check for a folder inside ./output/ by running ls ./output/. The folder name matches the slugified title from your sample doc.
- If Claude Code skipped the Copier step entirely, open SKILL.md and confirm the orchestration instructions in sections 5 and 6 match what you wrote in the previous step.
My /forge-repo skill ran but the output directory is empty or missing files. Help me debug.
Run the conformance checker and review the output
The SKILL.md orchestrator already runs the conformance checker as part of its pipeline. Running it manually gives you an independent verification of the score and a chance to inspect the rubric results yourself.
- Run the conformance checker against the generated output by running this command in your terminal (not inside Claude Code):
python src/conformance_checker.py ./output/build-a-cloud-resume/ intern
What does this command do?
- The first argument (./output/build-a-cloud-resume/) points to the generated repo directory.
- The second argument (intern) tells the checker which tier to evaluate against. The checker only scores rubric axes that apply to that tier.
- For intern tier, 8 of the 16 axes apply. The target score is 75%, meaning at least 6 of those 8 axes need to pass.
You should see a JSON report printed to the terminal. Look for the score and meets_target fields.
What should a passing report look like?
A successful intern-tier run looks something like this:
{
"tier": "intern",
"score": 100,
"target": 75,
"passed": 8,
"applicable": 8,
"meets_target": true,
"failures": []
}
A score of 100 means all 8 intern-tier axes passed. Your score might be slightly different if any axes failed. As long as meets_target is true, you are above the 75% intern threshold.
Score below 75%?
Check the failures array in the JSON output. It lists every axis that did not pass. Common failures and their fixes:
- "README has badges" failed: Open README.md.jinja in your template and confirm the badge markdown () is on the line right after the title.
- "NextWork credit present" failed: Make sure your template includes nextwork-credit.md.jinja (not nextwork_credit.md.jinja). The checker looks for a hyphenated filename.
- "CONTRIBUTING exists" failed: Confirm CONTRIBUTING.md.jinja exists in your templates/portfolio-repo/ directory.
After fixing any template issues, delete the output folder and re-run /forge-repo test-docs/sample-project.md in Claude Code to regenerate.
My conformance checker score is below 75% for intern tier. Help me fix the failing axes.
Now take a look inside the generated files to confirm the content quality matches the structure.
- Open the generated README by running:
cat ./output/build-a-cloud-resume/README.md
- Confirm the README follows cognitive-funnel order: a title at the top, badge(s) below it, a one-line description, a "What This Project Does" section, a "Built With" list, and the NextWork credit footer at the bottom.
- Open the NextWork credit file by running:
cat ./output/build-a-cloud-resume/nextwork-credit.md
- Confirm the credit file includes the project title, author name, tier, and a link to NextWork.
Why review manually after a passing score?
The conformance checker verifies that files exist and have the right structural markers (a title, a badge, a description line). It does not evaluate the quality of the content inside those files.
Manual review catches issues like the structurer agent producing a vague summary, the credit writer dropping the author name, or the diagram agent generating an empty Mermaid block. A 100% conformance score with poor content still looks bad to a recruiter.
✔️ Awesome, I've got everything!
Make sure you have saved your test-docs/sample-project.md file. The generated output in ./output/ is created by the pipeline and does not need manual saving.
ⓧ I'd like to double check the full code
Here is the complete sample doc you should have in test-docs/sample-project.md:
# Build a Cloud Resume
## Introducing Today's Project
In this project, I built a cloud-hosted resume website using Amazon S3 and CloudFront. The goal was to learn how to deploy static websites to the cloud and serve them through a content delivery network.
## Tools and Concepts
- **Amazon S3**: Object storage service used to host the static website files.
- **Amazon CloudFront**: CDN that serves the website with low latency globally.
- **HTML/CSS**: Used to build the resume page layout and styling.
- **AWS CLI**: Command-line tool for uploading files to S3.
## Project Reflection
I chose S3 for static hosting because it removes the need to manage a web server. This was simpler than setting up an EC2 instance with Nginx. CloudFront was added to enable HTTPS and improve load times for users around the world. One challenge was configuring the bucket policy to allow public reads while restricting writes to only authenticated users.
## Step 1: Set Up the S3 Bucket
I created a new S3 bucket in us-east-1 with static website hosting enabled. I set index.html as the default document and configured the bucket policy for public read access.
## Step 2: Build and Upload the Resume
I wrote the HTML and CSS for my resume page locally. Then I used the AWS CLI to sync my local files to the bucket with the aws s3 sync command.
## Step 3: Add CloudFront Distribution
I created a CloudFront distribution pointing to the S3 bucket origin. I set the default root object to index.html and waited for the distribution to finish deploying.
Secret mission
Add OpenSSF Scorecard, Vale, and commitlint
Your generated repos already hit 95% on the rubric. But professional open-source repos carry three extra trust signals: automated security scoring, prose quality linting, and enforced commit message standards. Wire all three into your Copier template so every senior-tier repo ships with production-grade quality gates.
Before you go
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs beyond your existing Claude Max subscription.
Resources you used:
- The portfolioforge/ project directory (source code, templates, subagent prompts, Claude Code skill).
- Python packages installed via pip: copier and pydantic.
- The output/ directory containing any generated portfolio repos.
- The test-docs/ directory with sample NextWork documents.
- Your gh CLI authentication session.
- Any GitHub repositories created via gh repo create during the end-to-end forge.
Keep everything running
No action needed. Choose this if you plan to keep forging portfolio repos from more NextWork projects.
- Your portfolioforge/ directory stays on your Desktop, ready for your next /forge-repo run.
- Your copier and pydantic packages remain installed and available for other Python projects.
- Your gh CLI stays authenticated for future GitHub operations.
Pause - I'll come back to this later
Free up disk space from generated output while keeping your project code intact.
- Delete the output/ directory inside portfolioforge/. Generated repos can always be re-forged from your source docs.
- Keep the portfolioforge/ directory, copier, pydantic, and gh CLI installed so you can pick up where you left off.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Delete the entire portfolioforge/ directory from your Desktop.
- Uninstall the Python packages by running:
pip uninstall copier pydantic -y
- Log out of the GitHub CLI by running:
gh auth logout
- If you created any test repos on GitHub during the end-to-end forge, delete them from your GitHub account under Settings on each repository page.
Mission accomplished
That's a Wrap!
Nice work! You just built PortfolioForge, a complete doc-to-repo pipeline that turns NextWork project documentation into recruiter-ready GitHub portfolio repos in under 90 seconds.
Here's what you learned:
- Built a Pydantic-powered doc parser and auto-tier detector that extracts structured data from freeform markdown and classifies projects by complexity.
- Authored a unified Copier template with Jinja2 conditionals that generates different file sets per tier, including READMEs, ADRs, Mermaid diagrams, and community-profile files.
- Designed a 5-subagent parallel prompt architecture orchestrated by a single /forge-repo Claude Code skill, complete with a 16-axis conformance checker and self-healing retry loop.
- Added OpenSSF Scorecard, Vale prose linting, and commitlint for production-grade repo quality.
Ready to quiz yourself?