Install
$ agentstack add skill-jasonchen0604-codebase-to-portfolio-generate-tech-profile-json ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Generate Tech Profile JSON Skill
Goal
Read all project docs (doc_filename) with the latest skill_version, extract structured frontmatter data, then generate a tech-profile/ split-file directory (schema version 2.0).
Output layout:
tech-profile/
├── meta.json # meta + profile + linkedin + id_map
├── domains-.json # one per configured language
├── tag-index-.json # one per configured language
├── product-groups-.json # one per configured language
└── projects/
├── ..json # one file per project, per configured language
Why split? Each file is 1–15KB. Incremental runs only rewrite changed project files + rebuild the index files. Token cost scales with changed projects, not total project count.
Incremental update logic:
- Load existing
tech-profile/meta.json→ getid_mapandsource_version - Load all
tech-profile/projects/*..json→ build in-memory cache - For each project: compute hash, compare cached
skill_version
- Unchanged → skip (reuse cached file, zero token cost)
- New or changed → read the doc file, write
..jsonfor each configured language
- Always rebuild index files (domains, tag-index, product-groups) from all project files
- Write
meta.jsonwith updated totals
On first run (no existing tech-profile/), process all projects.
Step 0: Read config
Read profile.config.json from the current working directory. If missing, tell the user to copy examples/profile.config.example.json to profile.config.json and fill it in, then stop.
profile.name,profile.email,profile.title,profile.years_of_experience— used directly in theprofileJSON block (ifyears_of_experienceis absent, derive from the oldest project's last commit)doc_filename(defaultCLAUDE.md)languages(default["en"])timezone(default+00:00)privacy_blocklist(default[])custom_domains(default{}) — merged into the built-in domain table in Step 4
JSON Schema
Each language's file set conforms to this schema:
{
"meta": {
"generated_at": "2026-06-19T12:00:00+00:00", // ISO8601, configured timezone
"source_version": "2.0",
"total_projects": 95,
"lang": "en"
},
"profile": {
"name": "",
"title": "",
"title_alt": "",
"email": "",
"summary": "", // 3–5 sentences, synthesized from all Highlights, in this file's language
"linkedin_about": "", // LinkedIn-ready About section, in this file's language
"years_of_experience": 8, // from config, or derived from oldest project last_commit
"total_projects": 95
},
"domains": [
{
"id": "frontend", // snake_case, stable identifier
"label": "Frontend", // translated per this file's language
"icon": "🖥",
"project_count": 10,
"summary": "", // domain expertise paragraph, in this file's language
"skills": [
{
"tag": "React",
"level": "expert", // "expert" | "proficient" | "familiar" — derived from project count + status
"project_count": 8, // MUST equal projects.length — derived from it, not estimated separately
"projects": [ // ALL project ids that have this tag — no sampling, no truncation
"project-management-system-frontend",
"lab-space-rack-management-frontend"
// ... every matching project id
]
}
]
}
],
"projects": [
{
"id": "3a7f2c1b09e4", // SHA-256[:12] of project folder path relative to scan_root
"name": "Project Management System — Frontend", // display_name (privacy-safe), never a brand/client name
"category": "Enterprise Web Application", // translated per this file's language
"status": "Production", // translated per this file's language
"status_badge": "[P]",
"featured": false,
"tags": ["React", "NestJS", "Azure AD", "WebSocket", "CASL"],
"core_tech": "TypeScript / React 18 + Vite 3",
"database": "PostgreSQL, Redis",
"deployment": "Docker Swarm / GitLab CI",
"description": "...", // translated per this file's language
"domain_primary": "frontend", // primary domain id
"domains": ["frontend", "backend"] // all applicable domain ids
}
],
"tag_index": [
{
"tag": "React",
"domain_id": "frontend",
"domain_label": "Frontend", // translated per this file's language
"project_count": 8,
"level": "expert"
}
],
"linkedin": {
"headline": " | key tech A · key tech B · key tech C", // translated per this file's language
"about": "", // same as profile.linkedin_about, formatted for direct paste
"skills_list": ["TypeScript", "React", "NestJS", "Next.js", "Docker", "PostgreSQL", "LangGraph", "Azure OpenAI"],
// top 20 skills sorted by: featured projects first, then project_count desc
"experience_highlights": [
// For each domain with ≥3 projects, one bullet point summarizing key projects by display_name (no raw project codes)
"Led development of an enterprise platform serving multiple business units, built with NestJS + Next.js + Docker Swarm."
]
},
"product_groups": [
{
"product_name": "Lab Space & Rack Management",
"project_count": 4,
"roles": ["Frontend", "Backend (Laravel)", "Backend (NestJS)", "Docker / Infra"],
"status": "Production",
"projects": [
{ "id": "lab-space-rack-management-frontend", "role": "Frontend", "status": "Production" }
]
// projects[] contains id + role + status only — full data lives in projects[]
}
]
}
Execution Steps
Step 1: Get the latest version
Run the /get-latest-version skill.
- If
none, abort and tell the user to run/batch-initfirst.
Step 2: Filter projects
Read projects-list.md, filter rows where:
- "Active" column is ✅
- Doc-file column is ✅
- "Version" column equals the latest version string
Collect each matching project's Path column. Expand ~ via $HOME.
Step 3: Read project doc frontmatter (batch up to 10 in parallel)
For each project, read / and extract:
project_name,category,status,tags(array),one_line_descriptioncore_tech,database,deployment,featured- Also extract the
Highlightsbullet text from## 📋 Portfolio Summarysection body - Also record
path— the project's absolute path (used for grouping in Step 3b)
After extracting fields, derive a display_name for each project:
Privacy-Safe Display Name Rules
The core rule: display_name must describe only system function — never a client, company, brand, or product name.
Two categories of names require sanitization:
Category 1 — Project code prefixes (structural identifiers in project_name):
- Patterns: any 2–3 letter prefix followed by 2 digits (e.g.
pa27,pt07,wp18) - Action: strip prefix, derive name from
one_line_description
Category 2 — Client/brand/product names (appear in project_name directly or as the display name after stripping suffixes):
- Names listed in the config's
privacy_blocklist— apply judgment for any unfamiliar name that looks like a client/company/brand token too - Also applies when the derived display name itself still contains a brand/client name — re-derive from
one_line_description - Action: replace with a purely functional description from
one_line_description
Deriving a safe display name:
- Extract the core system/feature name from
one_line_description— translate per each configured language's file - Append role suffix:
-web→— Frontend,-server→— Backend,-nest-server→— Backend (NestJS),-docker→— Docker / Infra,-ios/-android→— iOS App/— Android App; or derive fromcore_tech - If
one_line_descriptionalso contains the client/brand name, paraphrase around the function not the brand
Final check before writing output: scan every display_name against privacy_blocklist. If found, re-derive.
- Assign each project a stable
id:SHA-256(relPath)[:12]whererelPathis the project folder path relative toscan_root(e.g."clients/acme/acme-lab-space-and-rack-management-web"). This keeps IDs opaque (no brand/client leak) and stable (path doesn't change unless project is moved). In case of collision (extremely rare), append__Nsuffix before re-hashing. - Use
display_nameas thenamefield inprojects[]. Store the originalproject_nameasraw_namefor internal reference only (omit from output JSON). - In
domains[].skills[].projects[]: store only theidstrings — no other fields. Consumer looks up full data inprojects[]by id. - In
product_groups[].projects[]: store{ id, role, status }only — nodisplay_namefield.
Step 3b: Group projects by product
After deriving display names, group projects belonging to the same product/system:
Grouping logic (apply both; either match triggers grouping):
- Projects sharing the same parent directory path → one product group
- Projects whose
project_nameshares a common prefix after stripping role suffixes (-web,-server,-nest-server,-docker,-backend,-frontend,-api,-cli) → one product group
For each group:
product_name: the shared functional name without role suffix (anonymized per display name rules)projects: array of members, each withdisplay_name,role(Frontend / Backend / Backend NestJS / Docker / Infra / CLI), andstatus
Add a product_groups top-level array to every language's file set (after tag_index):
"product_groups": [
{
"product_name": "Lab Space & Rack Management",
"project_count": 4,
"roles": ["Frontend", "Backend (Laravel)", "Backend (NestJS)", "Docker / Infra"],
"status": "Production", // highest-priority status among members
"projects": [
{ "id": "3a7f2c1b09e4", "role": "Frontend", "status": "Production" },
{ "id": "9f1bc234a5e7", "role": "Backend (Laravel)", "status": "Production" },
{ "id": "c82d0f6173ab", "role": "Backend (NestJS)", "status": "Production" },
{ "id": "7e45a912b0cf", "role": "Docker / Infra", "status": "Production" }
]
}
]
Individual projects still appear in the projects[] array — product_groups is additive.
Step 4: Build domain mapping
Use the domain → tags mapping from the generate-tech-profile skill (same built-in table), merged with custom_domains from config.
For each tag in any project, assign it to a domain. A tag may belong to multiple domains.
Step 5: Derive skill levels
For each (domain, tag) pair, collect every project that has this tag — no sampling, no truncation. Then:
- Set
projects= the complete array of those project ids (strings only) — ALL of them - Set
project_count=projects.lengthexactly — this is the source of truth, not a separate estimate - Derive
levelfromproject_countand the statuses of the matched projects: expert:project_count≥ 5, and ≥1 is Production or In Progressproficient:project_count2–4, orproject_count= 1 with Production statusfamiliar:project_count= 1, status is Archived / Prototype / Completed only
Step 6: Derive yearsofexperience
Use profile.years_of_experience from config if set. Otherwise, not reliably derivable from project docs — output null and note in the summary that the user should set it manually.
Step 7: Build profile.summary and linkedin fields
Synthesize from all Highlights texts, once per configured language:
profile.summary: 3–5 sentence paragraph covering top domains, key technologies, and scalelinkedin.about: LinkedIn-optimized About section, 3–5 sentences, first-person, action verbs
Step 8: Build linkedin.skills_list
Take all tags, sort by:
- Tags appearing in
featured: trueprojects first - Then by project_count descending
Take top 20.
Step 9: Build linkedin.experience_highlights
For each domain with ≥3 projects, write one bullet, first-person, action verb, mentioning key projects by display name, including scale/impact if available, in this file's language.
Step 10: Write split files
Write to tech-profile/ under the current working directory:
tech-profile/projects/..jsonfor each project and each configured language (only new/changed ones on incremental runs)tech-profile/domains-.jsonfor each configured languagetech-profile/tag-index-.jsonfor each configured languagetech-profile/product-groups-.jsonfor each configured languagetech-profile/meta.json— includeid_map: { "": "" }for all projects
meta.json structure:
{
"meta": { "generated_at", "source_version", "total_projects", "schema_version": "2.0", "lang": "" },
"profile": { ...same as before... },
"linkedin": { ...same as before... },
"id_map": {
"3a7f2c1b09e4": "Lab Space & Rack Management — Frontend",
...
}
}
Use 2-space indentation. Ensure valid JSON (no trailing commas, no comments).
Step 11: Report results
✅ Tech profile JSON generated (split-file format)
- Projects: N total, M updated (skill_version: X.Y)
- Unique tags: T
- Domains: [frontend, backend, ai_llm, ...]
- Languages: [en, ...]
- tech-profile/ →
├── meta.json (id_map: N entries)
├── domains-.json (one per language)
├── tag-index-.json (one per language)
├── product-groups-.json (one per language)
└── projects/ (N × languages files)
Domain IDs and Labels
| id | Label | |----|----------| | frontend | Frontend | | backend | Backend | | ai_llm | AI / LLM | | database | Database | | devops | DevOps / Infrastructure | | cloud | Cloud Services | | mobile | Mobile | | tools | Tools & Automation | | languages | Languages | | other | Other |
Translate labels per configured language using the same translation approach as generate-tech-profile (Domain name translation table). Use icon: 🖥 frontend, ⚙️ backend, 🤖 ai_llm, 🗄 database, 🚀 devops, ☁️ cloud, 📱 mobile, 🔧 tools, 💻 languages, 📦 other.
Cloud domain tags
Tags that belong to cloud (remove from other/mobile/ai_llm if present): Firebase, Firebase Admin, Firestore, Firebase FCM, Firebase Hosting, Azure AD, Azure OpenAI, Azure Functions, Google Cloud Storage, Google Maps, Google Sign-In, Gmail API, Google Maps Geocoding API, Serverless, Self-Hosted
Status Translation
| English | badge | |----|-------| | Production | [P] | | In Progress | [IP] | | Maintenance | [M] | | Side Project | [S] | | Archived | [A] | | Prototype | [Proto] | | Completed | [C] |
Translate status labels per configured language using the same translation table as generate-tech-profile.
Notes
- Read project docs in parallel batches of up to 10
- Write project files in parallel batches; write index files after all projects are done
- Skip malformed doc files, log warning, continue
- For each non-source language file: translate all description/summary text into natural target-language text
- For the file matching the source data's original language: descriptions may be used as-is; still translate domain/status labels
featured: trueprojects are marked with"featured": truein the JSON — do NOT add ⭐ emoji inside JSON strings- Always overwrite index files (domains, tag-index, product-groups, meta) on each run; project files are only rewritten when changed
- The
linkedin_aboutfield must be copy-paste ready — no placeholders, no markdown formatting, plain text only tag_indexmust be sorted byproject_countdescendingprojectsarray sorted by: status priority (Production first)
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: jasonChen0604
- Source: jasonChen0604/codebase-to-portfolio
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.