feat: add developer profiling pipeline (#1084)

Analyze Claude Code session history to build behavioral profiles across
8 dimensions (communication, decisions, debugging, UX, vendor choices,
frustrations, learning style, explanation depth). Profiles generate
USER-PROFILE.md, dev-preferences command, and CLAUDE.md profile section.

New modules:
- profile-pipeline.cjs: session scanning, message extraction, sampling
- profile-output.cjs: profile rendering, questionnaire, artifact generation

New files:
- agents/gsd-user-profiler.md: profiler agent (8-dimension scoring)
- commands/gsd/profile-user.md: user-facing command
- workflows/profile-user.md: orchestration workflow
- templates/: user-profile, dev-preferences, claude-md templates
- references/user-profiling.md: detection heuristics reference

8 new CLI commands: scan-sessions, extract-messages, profile-sample,
write-profile, profile-questionnaire, generate-dev-preferences,
generate-claude-profile, generate-claude-md

Co-authored-by: TÂCHES <afromanguy@me.com>
This commit is contained in:
jecanore
2026-03-16 10:46:00 -05:00
committed by GitHub
parent a2f359e94b
commit 4cf6c04a3e
11 changed files with 3180 additions and 2 deletions

171
agents/gsd-user-profiler.md Normal file
View File

@@ -0,0 +1,171 @@
---
name: gsd-user-profiler
description: Analyzes extracted session messages across 8 behavioral dimensions to produce a scored developer profile with confidence levels and evidence. Spawned by profile orchestration workflows.
tools: Read
color: magenta
---
<role>
You are a GSD user profiler. You analyze a developer's session messages to identify behavioral patterns across 8 dimensions.
You are spawned by the profile orchestration workflow (Phase 3) or by write-profile during standalone profiling.
Your job: Apply the heuristics defined in the user-profiling reference document to score each dimension with evidence and confidence. Return structured JSON analysis.
CRITICAL: You must apply the rubric defined in the reference document. Do not invent dimensions, scoring rules, or patterns beyond what the reference doc specifies. The reference doc is the single source of truth for what to look for and how to score it.
</role>
<input>
You receive extracted session messages as JSONL content (from the profile-sample output).
Each message has the following structure:
```json
{
"sessionId": "string",
"projectPath": "encoded-path-string",
"projectName": "human-readable-project-name",
"timestamp": "ISO-8601",
"content": "message text (max 500 chars for profiling)"
}
```
Key characteristics of the input:
- Messages are already filtered to genuine user messages only (system messages, tool results, and Claude responses are excluded)
- Each message is truncated to 500 characters for profiling purposes
- Messages are project-proportionally sampled -- no single project dominates
- Recency weighting has been applied during sampling (recent sessions are overrepresented)
- Typical input size: 100-150 representative messages across all projects
</input>
<reference>
@get-shit-done/references/user-profiling.md
This is the detection heuristics rubric. Read it in full before analyzing any messages. It defines:
- The 8 dimensions and their rating spectrums
- Signal patterns to look for in messages
- Detection heuristics for classifying ratings
- Confidence scoring thresholds
- Evidence curation rules
- Output schema
</reference>
<process>
<step name="load_rubric">
Read the user-profiling reference document at `get-shit-done/references/user-profiling.md` to load:
- All 8 dimension definitions with rating spectrums
- Signal patterns and detection heuristics per dimension
- Confidence scoring thresholds (HIGH: 10+ signals across 2+ projects, MEDIUM: 5-9, LOW: <5, UNSCORED: 0)
- Evidence curation rules (combined Signal+Example format, 3 quotes per dimension, ~100 char quotes)
- Sensitive content exclusion patterns
- Recency weighting guidelines
- Output schema
</step>
<step name="read_messages">
Read all provided session messages from the input JSONL content.
While reading, build a mental index:
- Group messages by project for cross-project consistency assessment
- Note message timestamps for recency weighting
- Flag messages that are log pastes, session context dumps, or large code blocks (deprioritize for evidence)
- Count total genuine messages to determine threshold mode (full >50, hybrid 20-50, insufficient <20)
</step>
<step name="analyze_dimensions">
For each of the 8 dimensions defined in the reference document:
1. **Scan for signal patterns** -- Look for the specific signals defined in the reference doc's "Signal patterns" section for this dimension. Count occurrences.
2. **Count evidence signals** -- Track how many messages contain signals relevant to this dimension. Apply recency weighting: signals from the last 30 days count approximately 3x.
3. **Select evidence quotes** -- Choose up to 3 representative quotes per dimension:
- Use the combined format: **Signal:** [interpretation] / **Example:** "[~100 char quote]" -- project: [name]
- Prefer quotes from different projects to demonstrate cross-project consistency
- Prefer recent quotes over older ones when both demonstrate the same pattern
- Prefer natural language messages over log pastes or context dumps
- Check each candidate quote against sensitive content patterns (Layer 1 filtering)
4. **Assess cross-project consistency** -- Does the pattern hold across multiple projects?
- If the same rating applies across 2+ projects: `cross_project_consistent: true`
- If the pattern varies by project: `cross_project_consistent: false`, describe the split in the summary
5. **Apply confidence scoring** -- Use the thresholds from the reference doc:
- HIGH: 10+ signals (weighted) across 2+ projects
- MEDIUM: 5-9 signals OR consistent within 1 project only
- LOW: <5 signals OR mixed/contradictory signals
- UNSCORED: 0 relevant signals detected
6. **Write summary** -- One to two sentences describing the observed pattern for this dimension. Include context-dependent notes if applicable.
7. **Write claude_instruction** -- An imperative directive for Claude's consumption. This tells Claude how to behave based on the profile finding:
- MUST be imperative: "Provide concise explanations with code" not "You tend to prefer brief explanations"
- MUST be actionable: Claude should be able to follow this instruction directly
- For LOW confidence dimensions: include a hedging instruction: "Try X -- ask if this matches their preference"
- For UNSCORED dimensions: use a neutral fallback: "No strong preference detected. Ask the developer when this dimension is relevant."
</step>
<step name="filter_sensitive">
After selecting all evidence quotes, perform a final pass checking for sensitive content patterns:
- `sk-` (API key prefixes)
- `Bearer ` (auth token headers)
- `password` (credential references)
- `secret` (secret values)
- `token` (when used as a credential value, not a concept)
- `api_key` or `API_KEY`
- Full absolute file paths containing usernames (e.g., `/Users/john/`, `/home/john/`)
If any selected quote contains these patterns:
1. Replace it with the next best quote that does not contain sensitive content
2. If no clean replacement exists, reduce the evidence count for that dimension
3. Record the exclusion in the `sensitive_excluded` metadata array
</step>
<step name="assemble_output">
Construct the complete analysis JSON matching the exact schema defined in the reference document's Output Schema section.
Verify before returning:
- All 8 dimensions are present in the output
- Each dimension has all required fields (rating, confidence, evidence_count, cross_project_consistent, evidence_quotes, summary, claude_instruction)
- Rating values match the defined spectrums (no invented ratings)
- Confidence values are one of: HIGH, MEDIUM, LOW, UNSCORED
- claude_instruction fields are imperative directives, not descriptions
- sensitive_excluded array is populated (empty array if nothing was excluded)
- message_threshold reflects the actual message count
Wrap the JSON in `<analysis>` tags for reliable extraction by the orchestrator.
</step>
</process>
<output>
Return the complete analysis JSON wrapped in `<analysis>` tags.
Format:
```
<analysis>
{
"profile_version": "1.0",
"analyzed_at": "...",
...full JSON matching reference doc schema...
}
</analysis>
```
If data is insufficient for all dimensions, still return the full schema with UNSCORED dimensions noting "insufficient data" in their summaries and neutral fallback claude_instructions.
Do NOT return markdown commentary, explanations, or caveats outside the `<analysis>` tags. The orchestrator parses the tags programmatically.
</output>
<constraints>
- Never select evidence quotes containing sensitive patterns (sk-, Bearer, password, secret, token as credential, api_key, full file paths with usernames)
- Never invent evidence or fabricate quotes -- every quote must come from actual session messages
- Never rate a dimension HIGH without 10+ signals (weighted) across 2+ projects
- Never invent dimensions beyond the 8 defined in the reference document
- Weight recent messages approximately 3x (last 30 days) per reference doc guidelines
- Report context-dependent splits rather than forcing a single rating when contradictory signals exist across projects
- claude_instruction fields must be imperative directives, not descriptions -- the profile is an instruction document for Claude's consumption
- Deprioritize log pastes, session context dumps, and large code blocks when selecting evidence
- When evidence is genuinely insufficient, report UNSCORED with "insufficient data" -- do not guess
</constraints>

View File

@@ -0,0 +1,46 @@
---
name: gsd:profile-user
description: Generate developer behavioral profile and create Claude-discoverable artifacts
argument-hint: "[--questionnaire] [--refresh]"
allowed-tools:
- Read
- Write
- Bash
- Glob
- Grep
- AskUserQuestion
- Task
---
<objective>
Generate a developer behavioral profile from session analysis (or questionnaire) and produce artifacts (USER-PROFILE.md, /gsd:dev-preferences, CLAUDE.md section) that personalize Claude's responses.
Routes to the profile-user workflow which orchestrates the full flow: consent gate, session analysis or questionnaire fallback, profile generation, result display, and artifact selection.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/profile-user.md
@~/.claude/get-shit-done/references/ui-brand.md
</execution_context>
<context>
Flags from $ARGUMENTS:
- `--questionnaire` -- Skip session analysis entirely, use questionnaire-only path
- `--refresh` -- Rebuild profile even when one exists, backup old profile, show dimension diff
</context>
<process>
Execute the profile-user workflow end-to-end.
The workflow handles all logic including:
1. Initialization and existing profile detection
2. Consent gate before session analysis
3. Session scanning and data sufficiency checks
4. Session analysis (profiler agent) or questionnaire fallback
5. Cross-project split resolution
6. Profile writing to USER-PROFILE.md
7. Result display with report card and highlights
8. Artifact selection (dev-preferences, CLAUDE.md sections)
9. Sequential artifact generation
10. Summary with refresh diff (if applicable)
</process>

View File

@@ -139,6 +139,8 @@ const milestone = require('./lib/milestone.cjs');
const commands = require('./lib/commands.cjs');
const init = require('./lib/init.cjs');
const frontmatter = require('./lib/frontmatter.cjs');
const profilePipeline = require('./lib/profile-pipeline.cjs');
const profileOutput = require('./lib/profile-output.cjs');
// ─── CLI Router ───────────────────────────────────────────────────────────────
@@ -595,6 +597,94 @@ async function main() {
break;
}
// ─── Profiling Pipeline ────────────────────────────────────────────────
case 'scan-sessions': {
const pathIdx = args.indexOf('--path');
const sessionsPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
const verboseFlag = args.includes('--verbose');
const jsonFlag = args.includes('--json');
await profilePipeline.cmdScanSessions(sessionsPath, { verbose: verboseFlag, json: jsonFlag }, raw);
break;
}
case 'extract-messages': {
const sessionIdx = args.indexOf('--session');
const sessionId = sessionIdx !== -1 ? args[sessionIdx + 1] : null;
const limitIdx = args.indexOf('--limit');
const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : null;
const pathIdx = args.indexOf('--path');
const sessionsPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
const projectArg = args[1];
if (!projectArg || projectArg.startsWith('--')) {
error('Usage: gsd-tools extract-messages <project> [--session <id>] [--limit N] [--path <dir>]\nRun scan-sessions first to see available projects.');
}
await profilePipeline.cmdExtractMessages(projectArg, { sessionId, limit }, raw, sessionsPath);
break;
}
case 'profile-sample': {
const pathIdx = args.indexOf('--path');
const sessionsPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
const limitIdx = args.indexOf('--limit');
const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : 150;
const maxPerIdx = args.indexOf('--max-per-project');
const maxPerProject = maxPerIdx !== -1 ? parseInt(args[maxPerIdx + 1], 10) : null;
const maxCharsIdx = args.indexOf('--max-chars');
const maxChars = maxCharsIdx !== -1 ? parseInt(args[maxCharsIdx + 1], 10) : 500;
await profilePipeline.cmdProfileSample(sessionsPath, { limit, maxPerProject, maxChars }, raw);
break;
}
// ─── Profile Output ──────────────────────────────────────────────────
case 'write-profile': {
const inputIdx = args.indexOf('--input');
const inputPath = inputIdx !== -1 ? args[inputIdx + 1] : null;
if (!inputPath) error('--input <analysis-json-path> is required');
const outputIdx = args.indexOf('--output');
const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
profileOutput.cmdWriteProfile(cwd, { input: inputPath, output: outputPath }, raw);
break;
}
case 'profile-questionnaire': {
const answersIdx = args.indexOf('--answers');
const answers = answersIdx !== -1 ? args[answersIdx + 1] : null;
profileOutput.cmdProfileQuestionnaire({ answers }, raw);
break;
}
case 'generate-dev-preferences': {
const analysisIdx = args.indexOf('--analysis');
const analysisPath = analysisIdx !== -1 ? args[analysisIdx + 1] : null;
const outputIdx = args.indexOf('--output');
const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
const stackIdx = args.indexOf('--stack');
const stack = stackIdx !== -1 ? args[stackIdx + 1] : null;
profileOutput.cmdGenerateDevPreferences(cwd, { analysis: analysisPath, output: outputPath, stack }, raw);
break;
}
case 'generate-claude-profile': {
const analysisIdx = args.indexOf('--analysis');
const analysisPath = analysisIdx !== -1 ? args[analysisIdx + 1] : null;
const outputIdx = args.indexOf('--output');
const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
const globalFlag = args.includes('--global');
profileOutput.cmdGenerateClaudeProfile(cwd, { analysis: analysisPath, output: outputPath, global: globalFlag }, raw);
break;
}
case 'generate-claude-md': {
const outputIdx = args.indexOf('--output');
const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
const autoFlag = args.includes('--auto');
const forceFlag = args.includes('--force');
profileOutput.cmdGenerateClaudeMd(cwd, { output: outputPath, auto: autoFlag, force: forceFlag }, raw);
break;
}
default:
error(`Unknown command: ${command}`);
}

View File

@@ -0,0 +1,930 @@
/**
* Profile Output — profile rendering, questionnaire, and artifact generation
*
* Renders profiling analysis into user-facing artifacts:
* - write-profile: USER-PROFILE.md from analysis JSON
* - profile-questionnaire: fallback when no sessions available
* - generate-dev-preferences: dev-preferences.md command artifact
* - generate-claude-profile: Developer Profile section in CLAUDE.md
* - generate-claude-md: full CLAUDE.md with managed sections
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const { output, error, safeReadFile } = require('./core.cjs');
// ─── Constants ────────────────────────────────────────────────────────────────
const DIMENSION_KEYS = [
'communication_style', 'decision_speed', 'explanation_depth',
'debugging_approach', 'ux_philosophy', 'vendor_philosophy',
'frustration_triggers', 'learning_style'
];
const PROFILING_QUESTIONS = [
{
dimension: 'communication_style',
header: 'Communication Style',
context: 'Think about the last few times you asked Claude to build or change something. How did you frame the request?',
question: 'When you ask Claude to build something, how much context do you typically provide?',
options: [
{ label: 'Minimal -- "fix the bug", "add dark mode", just say what\'s needed', value: 'a', rating: 'terse-direct' },
{ label: 'Some context -- explain what and why in a paragraph or two', value: 'b', rating: 'conversational' },
{ label: 'Detailed specs -- headers, numbered lists, problem analysis, constraints', value: 'c', rating: 'detailed-structured' },
{ label: 'It depends on the task -- simple tasks get short prompts, complex ones get detailed specs', value: 'd', rating: 'mixed' },
],
},
{
dimension: 'decision_speed',
header: 'Decision Making',
context: 'Think about times when Claude presented you with multiple options -- like choosing a library, picking an architecture, or selecting an approach.',
question: 'When Claude presents you with options, how do you typically decide?',
options: [
{ label: 'Pick quickly based on gut feeling or past experience', value: 'a', rating: 'fast-intuitive' },
{ label: 'Ask for a comparison table or pros/cons, then decide', value: 'b', rating: 'deliberate-informed' },
{ label: 'Research independently (read docs, check GitHub stars) before deciding', value: 'c', rating: 'research-first' },
{ label: 'Let Claude recommend -- I generally trust the suggestion', value: 'd', rating: 'delegator' },
],
},
{
dimension: 'explanation_depth',
header: 'Explanation Preferences',
context: 'Think about when Claude explains code it wrote or an approach it took. How much detail feels right?',
question: 'When Claude explains something, how much detail do you want?',
options: [
{ label: 'Just the code -- I\'ll read it and figure it out myself', value: 'a', rating: 'code-only' },
{ label: 'Brief explanation with the code -- a sentence or two about the approach', value: 'b', rating: 'concise' },
{ label: 'Detailed walkthrough -- explain the approach, trade-offs, and code structure', value: 'c', rating: 'detailed' },
{ label: 'Deep dive -- teach me the concepts behind it so I understand the fundamentals', value: 'd', rating: 'educational' },
],
},
{
dimension: 'debugging_approach',
header: 'Debugging Style',
context: 'Think about the last few times something broke in your code. How did you approach it with Claude?',
question: 'When something breaks, how do you typically approach debugging with Claude?',
options: [
{ label: 'Paste the error and say "fix it" -- get it working fast', value: 'a', rating: 'fix-first' },
{ label: 'Share the error plus context, ask Claude to diagnose what went wrong', value: 'b', rating: 'diagnostic' },
{ label: 'Investigate myself first, then ask Claude about my specific theories', value: 'c', rating: 'hypothesis-driven' },
{ label: 'Walk through the code together step by step to understand the issue', value: 'd', rating: 'collaborative' },
],
},
{
dimension: 'ux_philosophy',
header: 'UX Philosophy',
context: 'Think about user-facing features you have built recently. How did you balance functionality with design?',
question: 'When building user-facing features, what do you prioritize?',
options: [
{ label: 'Get it working first, polish the UI later (or never)', value: 'a', rating: 'function-first' },
{ label: 'Basic usability from the start -- nothing ugly, but no pixel-perfection', value: 'b', rating: 'pragmatic' },
{ label: 'Design and UX are as important as functionality -- I care about the experience', value: 'c', rating: 'design-conscious' },
{ label: 'I mostly build backend, CLI, or infrastructure -- UX is minimal', value: 'd', rating: 'backend-focused' },
],
},
{
dimension: 'vendor_philosophy',
header: 'Library & Vendor Choices',
context: 'Think about the last time you needed a library or service for a project. How did you go about choosing it?',
question: 'When choosing libraries or services, what is your typical approach?',
options: [
{ label: 'Use whatever Claude suggests -- speed matters more than the perfect choice', value: 'a', rating: 'pragmatic-fast' },
{ label: 'Prefer well-known, battle-tested options (React, PostgreSQL, Express)', value: 'b', rating: 'conservative' },
{ label: 'Research alternatives, read docs, compare benchmarks before committing', value: 'c', rating: 'thorough-evaluator' },
{ label: 'Strong opinions -- I already know what I like and I stick with it', value: 'd', rating: 'opinionated' },
],
},
{
dimension: 'frustration_triggers',
header: 'Frustration Triggers',
context: 'Think about moments when working with AI coding assistants that made you frustrated or annoyed.',
question: 'What frustrates you most when working with AI coding assistants?',
options: [
{ label: 'Doing things I didn\'t ask for -- adding features, refactoring code, scope creep', value: 'a', rating: 'scope-creep' },
{ label: 'Not following instructions precisely -- ignoring constraints or requirements I stated', value: 'b', rating: 'instruction-adherence' },
{ label: 'Over-explaining or being too verbose -- just give me the code and move on', value: 'c', rating: 'verbosity' },
{ label: 'Breaking working code while fixing something else -- regressions', value: 'd', rating: 'regression' },
],
},
{
dimension: 'learning_style',
header: 'Learning Preferences',
context: 'Think about encountering something new -- an unfamiliar library, a codebase you inherited, a concept you hadn\'t used before.',
question: 'When you encounter something new in your codebase, how do you prefer to learn about it?',
options: [
{ label: 'Read the code directly -- I figure things out by reading and experimenting', value: 'a', rating: 'self-directed' },
{ label: 'Ask Claude to explain the relevant parts to me', value: 'b', rating: 'guided' },
{ label: 'Read official docs and tutorials first, then try things', value: 'c', rating: 'documentation-first' },
{ label: 'See a working example, then modify it to understand how it works', value: 'd', rating: 'example-driven' },
],
},
];
const CLAUDE_INSTRUCTIONS = {
communication_style: {
'terse-direct': 'Keep responses concise and action-oriented. Skip lengthy preambles. Match this developer\'s direct style.',
'conversational': 'Use a natural conversational tone. Explain reasoning briefly alongside code. Engage with the developer\'s questions.',
'detailed-structured': 'Match this developer\'s structured communication: use headers for sections, numbered lists for steps, and acknowledge provided context before responding.',
'mixed': 'Adapt response detail to match the complexity of each request. Brief for simple tasks, detailed for complex ones.',
},
decision_speed: {
'fast-intuitive': 'Present a single strong recommendation with brief justification. Skip lengthy comparisons unless asked.',
'deliberate-informed': 'Present options in a structured comparison table with pros/cons. Let the developer make the final call.',
'research-first': 'Include links to docs, GitHub repos, or benchmarks when recommending tools. Support the developer\'s research process.',
'delegator': 'Make clear recommendations with confidence. Explain your reasoning briefly, but own the suggestion.',
},
explanation_depth: {
'code-only': 'Prioritize code output. Add comments inline rather than prose explanations. Skip walkthroughs unless asked.',
'concise': 'Pair code with a brief explanation (1-2 sentences) of the approach. Keep prose minimal.',
'detailed': 'Explain the approach, key trade-offs, and code structure alongside the implementation. Use headers to organize.',
'educational': 'Teach the underlying concepts and principles, not just the implementation. Relate new patterns to fundamentals.',
},
debugging_approach: {
'fix-first': 'Prioritize the fix. Show the corrected code first, then optionally explain what was wrong. Minimize diagnostic preamble.',
'diagnostic': 'Diagnose the root cause before presenting the fix. Explain what went wrong and why the fix addresses it.',
'hypothesis-driven': 'Engage with the developer\'s theories. Validate or refine their hypotheses before jumping to solutions.',
'collaborative': 'Walk through the debugging process step by step. Explain the investigation approach, not just the conclusion.',
},
ux_philosophy: {
'function-first': 'Focus on functionality and correctness. Keep UI minimal and functional. Skip design polish unless requested.',
'pragmatic': 'Build clean, usable interfaces without over-engineering. Apply basic design principles (spacing, alignment, contrast).',
'design-conscious': 'Invest in UX quality: thoughtful spacing, smooth transitions, responsive layouts. Treat design as a first-class concern.',
'backend-focused': 'Optimize for developer experience (clear APIs, good error messages, helpful CLI output) over visual design.',
},
vendor_philosophy: {
'pragmatic-fast': 'Suggest libraries quickly based on popularity and reliability. Don\'t over-analyze choices for non-critical dependencies.',
'conservative': 'Recommend well-established, widely-adopted tools with strong community support. Avoid bleeding-edge options.',
'thorough-evaluator': 'Compare alternatives with specific metrics (bundle size, GitHub stars, maintenance activity). Support informed decisions.',
'opinionated': 'Respect the developer\'s existing tool preferences. Ask before suggesting alternatives to their preferred stack.',
},
frustration_triggers: {
'scope-creep': 'Do exactly what is asked -- nothing more. Never add unrequested features, refactoring, or "improvements". Ask before expanding scope.',
'instruction-adherence': 'Follow instructions precisely. Re-read constraints before responding. If requirements conflict, flag the conflict rather than silently choosing.',
'verbosity': 'Be concise. Lead with code, follow with brief explanation only if needed. Avoid restating the problem or unnecessary context.',
'regression': 'Before modifying working code, verify the change is safe. Run existing tests mentally. Flag potential regression risks explicitly.',
},
learning_style: {
'self-directed': 'Point to relevant code sections and let the developer explore. Add signposts (file paths, function names) rather than full explanations.',
'guided': 'Explain concepts in context of the developer\'s codebase. Use their actual code as examples when teaching.',
'documentation-first': 'Link to official documentation and relevant sections. Structure explanations like reference material.',
'example-driven': 'Lead with working code examples. Show a minimal example first, then explain how to extend or modify it.',
},
};
const CLAUDE_MD_FALLBACKS = {
project: 'Project not yet initialized. Run /gsd:new-project to set up.',
stack: 'Technology stack not yet documented. Will populate after codebase mapping or first phase.',
conventions: 'Conventions not yet established. Will populate as patterns emerge during development.',
architecture: 'Architecture not yet mapped. Follow existing patterns found in the codebase.',
};
const CLAUDE_MD_PROFILE_PLACEHOLDER = [
'<!-- GSD:profile-start -->',
'## Developer Profile',
'',
'> Profile not yet configured. Run `/gsd:profile-user` to generate your developer profile.',
'> This section is managed by `generate-claude-profile` -- do not edit manually.',
'<!-- GSD:profile-end -->',
].join('\n');
// ─── Helper Functions ─────────────────────────────────────────────────────────
function isAmbiguousAnswer(dimension, value) {
if (dimension === 'communication_style' && value === 'd') return true;
const question = PROFILING_QUESTIONS.find(q => q.dimension === dimension);
if (!question) return false;
const option = question.options.find(o => o.value === value);
if (!option) return false;
return option.rating === 'mixed';
}
function generateClaudeInstruction(dimension, rating) {
const dimInstructions = CLAUDE_INSTRUCTIONS[dimension];
if (dimInstructions && dimInstructions[rating]) {
return dimInstructions[rating];
}
return `Adapt to this developer's ${dimension.replace(/_/g, ' ')} preference: ${rating}.`;
}
function extractSectionContent(fileContent, sectionName) {
const startMarker = `<!-- GSD:${sectionName}-start`;
const endMarker = `<!-- GSD:${sectionName}-end -->`;
const startIdx = fileContent.indexOf(startMarker);
const endIdx = fileContent.indexOf(endMarker);
if (startIdx === -1 || endIdx === -1) return null;
const startTagEnd = fileContent.indexOf('-->', startIdx);
if (startTagEnd === -1) return null;
return fileContent.substring(startTagEnd + 3, endIdx);
}
function buildSection(sectionName, sourceFile, content) {
return [
`<!-- GSD:${sectionName}-start source:${sourceFile} -->`,
content,
`<!-- GSD:${sectionName}-end -->`,
].join('\n');
}
function updateSection(fileContent, sectionName, newContent) {
const startMarker = `<!-- GSD:${sectionName}-start`;
const endMarker = `<!-- GSD:${sectionName}-end -->`;
const startIdx = fileContent.indexOf(startMarker);
const endIdx = fileContent.indexOf(endMarker);
if (startIdx !== -1 && endIdx !== -1) {
const before = fileContent.substring(0, startIdx);
const after = fileContent.substring(endIdx + endMarker.length);
return { content: before + newContent + after, action: 'replaced' };
}
return { content: fileContent.trimEnd() + '\n\n' + newContent + '\n', action: 'appended' };
}
function detectManualEdit(fileContent, sectionName, expectedContent) {
const currentContent = extractSectionContent(fileContent, sectionName);
if (currentContent === null) return false;
const normalize = (s) => s.trim().replace(/\n{3,}/g, '\n\n');
return normalize(currentContent) !== normalize(expectedContent);
}
function extractMarkdownSection(content, sectionName) {
if (!content) return null;
const lines = content.split('\n');
let capturing = false;
const result = [];
const headingPattern = new RegExp(`^## ${sectionName}\\s*$`);
for (const line of lines) {
if (headingPattern.test(line)) {
capturing = true;
result.push(line);
continue;
}
if (capturing && /^## /.test(line)) break;
if (capturing) result.push(line);
}
return result.length > 0 ? result.join('\n').trim() : null;
}
// ─── CLAUDE.md Section Generators ─────────────────────────────────────────────
function generateProjectSection(cwd) {
const projectPath = path.join(cwd, '.planning', 'PROJECT.md');
const content = safeReadFile(projectPath);
if (!content) {
return { content: CLAUDE_MD_FALLBACKS.project, source: 'PROJECT.md', hasFallback: true };
}
const parts = [];
const h1Match = content.match(/^# (.+)$/m);
if (h1Match) parts.push(`**${h1Match[1]}**`);
const whatThisIs = extractMarkdownSection(content, 'What This Is');
if (whatThisIs) {
const body = whatThisIs.replace(/^## What This Is\s*/i, '').trim();
if (body) parts.push(body);
}
const coreValue = extractMarkdownSection(content, 'Core Value');
if (coreValue) {
const body = coreValue.replace(/^## Core Value\s*/i, '').trim();
if (body) parts.push(`**Core Value:** ${body}`);
}
const constraints = extractMarkdownSection(content, 'Constraints');
if (constraints) {
const body = constraints.replace(/^## Constraints\s*/i, '').trim();
if (body) parts.push(`### Constraints\n\n${body}`);
}
if (parts.length === 0) {
return { content: CLAUDE_MD_FALLBACKS.project, source: 'PROJECT.md', hasFallback: true };
}
return { content: parts.join('\n\n'), source: 'PROJECT.md', hasFallback: false };
}
function generateStackSection(cwd) {
const codebasePath = path.join(cwd, '.planning', 'codebase', 'STACK.md');
const researchPath = path.join(cwd, '.planning', 'research', 'STACK.md');
let content = safeReadFile(codebasePath);
let source = 'codebase/STACK.md';
if (!content) {
content = safeReadFile(researchPath);
source = 'research/STACK.md';
}
if (!content) {
return { content: CLAUDE_MD_FALLBACKS.stack, source: 'STACK.md', hasFallback: true };
}
const lines = content.split('\n');
const summaryLines = [];
let inTable = false;
for (const line of lines) {
if (line.startsWith('#')) {
if (!line.startsWith('# ') || summaryLines.length > 0) summaryLines.push(line);
continue;
}
if (line.startsWith('|')) { inTable = true; summaryLines.push(line); continue; }
if (inTable && line.trim() === '') inTable = false;
if (line.startsWith('- ') || line.startsWith('* ')) summaryLines.push(line);
}
const summary = summaryLines.length > 0 ? summaryLines.join('\n') : content.trim();
return { content: summary, source, hasFallback: false };
}
function generateConventionsSection(cwd) {
const conventionsPath = path.join(cwd, '.planning', 'codebase', 'CONVENTIONS.md');
const content = safeReadFile(conventionsPath);
if (!content) {
return { content: CLAUDE_MD_FALLBACKS.conventions, source: 'CONVENTIONS.md', hasFallback: true };
}
const lines = content.split('\n');
const summaryLines = [];
for (const line of lines) {
if (line.startsWith('#')) { if (!line.startsWith('# ')) summaryLines.push(line); continue; }
if (line.startsWith('- ') || line.startsWith('* ') || line.startsWith('|')) summaryLines.push(line);
}
const summary = summaryLines.length > 0 ? summaryLines.join('\n') : content.trim();
return { content: summary, source: 'CONVENTIONS.md', hasFallback: false };
}
function generateArchitectureSection(cwd) {
const architecturePath = path.join(cwd, '.planning', 'codebase', 'ARCHITECTURE.md');
const content = safeReadFile(architecturePath);
if (!content) {
return { content: CLAUDE_MD_FALLBACKS.architecture, source: 'ARCHITECTURE.md', hasFallback: true };
}
const lines = content.split('\n');
const summaryLines = [];
for (const line of lines) {
if (line.startsWith('#')) { if (!line.startsWith('# ')) summaryLines.push(line); continue; }
if (line.startsWith('- ') || line.startsWith('* ') || line.startsWith('|') || line.startsWith('```')) summaryLines.push(line);
}
const summary = summaryLines.length > 0 ? summaryLines.join('\n') : content.trim();
return { content: summary, source: 'ARCHITECTURE.md', hasFallback: false };
}
// ─── Commands ─────────────────────────────────────────────────────────────────
function cmdWriteProfile(cwd, options, raw) {
if (!options.input) {
error('--input <analysis-json-path> is required');
}
let analysisPath = options.input;
if (!path.isAbsolute(analysisPath)) analysisPath = path.join(cwd, analysisPath);
if (!fs.existsSync(analysisPath)) error(`Analysis file not found: ${analysisPath}`);
let analysis;
try {
analysis = JSON.parse(fs.readFileSync(analysisPath, 'utf-8'));
} catch (err) {
error(`Failed to parse analysis JSON: ${err.message}`);
}
if (!analysis.dimensions || typeof analysis.dimensions !== 'object') {
error('Analysis JSON must contain a "dimensions" object');
}
if (!analysis.profile_version) {
error('Analysis JSON must contain "profile_version"');
}
const SENSITIVE_PATTERNS = [
/sk-[a-zA-Z0-9]{20,}/g,
/Bearer\s+[a-zA-Z0-9._-]+/gi,
/password\s*[:=]\s*\S+/gi,
/secret\s*[:=]\s*\S+/gi,
/token\s*[:=]\s*\S+/gi,
/api[_-]?key\s*[:=]\s*\S+/gi,
/\/Users\/[a-zA-Z0-9._-]+\//g,
/\/home\/[a-zA-Z0-9._-]+\//g,
/ghp_[a-zA-Z0-9]{36}/g,
/gho_[a-zA-Z0-9]{36}/g,
/xoxb-[a-zA-Z0-9-]+/g,
];
let redactedCount = 0;
function redactSensitive(text) {
if (typeof text !== 'string') return text;
let result = text;
for (const pattern of SENSITIVE_PATTERNS) {
pattern.lastIndex = 0;
const matches = result.match(pattern);
if (matches) {
redactedCount += matches.length;
result = result.replace(pattern, '[REDACTED]');
}
}
return result;
}
for (const dimKey of Object.keys(analysis.dimensions)) {
const dim = analysis.dimensions[dimKey];
if (dim.evidence && Array.isArray(dim.evidence)) {
for (let i = 0; i < dim.evidence.length; i++) {
const ev = dim.evidence[i];
if (ev.quote) ev.quote = redactSensitive(ev.quote);
if (ev.example) ev.example = redactSensitive(ev.example);
if (ev.signal) ev.signal = redactSensitive(ev.signal);
}
}
}
if (redactedCount > 0) {
process.stderr.write(`Sensitive content redacted: ${redactedCount} pattern(s) removed from evidence quotes\n`);
}
const templatePath = path.join(__dirname, '..', 'templates', 'user-profile.md');
if (!fs.existsSync(templatePath)) error(`Template not found: ${templatePath}`);
let template = fs.readFileSync(templatePath, 'utf-8');
const dimensionLabels = {
communication_style: 'Communication',
decision_speed: 'Decisions',
explanation_depth: 'Explanations',
debugging_approach: 'Debugging',
ux_philosophy: 'UX Philosophy',
vendor_philosophy: 'Vendor Philosophy',
frustration_triggers: 'Frustration Triggers',
learning_style: 'Learning Style',
};
const summaryLines = [];
let highCount = 0, mediumCount = 0, lowCount = 0, dimensionsScored = 0;
for (const dimKey of DIMENSION_KEYS) {
const dim = analysis.dimensions[dimKey];
if (!dim) continue;
const conf = (dim.confidence || '').toUpperCase();
if (conf === 'HIGH' || conf === 'MEDIUM' || conf === 'LOW') dimensionsScored++;
if (conf === 'HIGH') {
highCount++;
if (dim.claude_instruction) summaryLines.push(`- **${dimensionLabels[dimKey] || dimKey}:** ${dim.claude_instruction} (HIGH)`);
} else if (conf === 'MEDIUM') {
mediumCount++;
if (dim.claude_instruction) summaryLines.push(`- **${dimensionLabels[dimKey] || dimKey}:** ${dim.claude_instruction} (MEDIUM)`);
} else if (conf === 'LOW') {
lowCount++;
}
}
const summaryInstructions = summaryLines.length > 0
? summaryLines.join('\n')
: '- No high or medium confidence dimensions scored yet.';
template = template.replace(/\{\{generated_at\}\}/g, new Date().toISOString());
template = template.replace(/\{\{data_source\}\}/g, analysis.data_source || 'session_analysis');
template = template.replace(/\{\{projects_list\}\}/g, (analysis.projects_list || []).join(', '));
template = template.replace(/\{\{message_count\}\}/g, String(analysis.message_count || 0));
template = template.replace(/\{\{summary_instructions\}\}/g, summaryInstructions);
template = template.replace(/\{\{profile_version\}\}/g, analysis.profile_version);
template = template.replace(/\{\{projects_count\}\}/g, String((analysis.projects_list || []).length));
template = template.replace(/\{\{dimensions_scored\}\}/g, String(dimensionsScored));
template = template.replace(/\{\{high_confidence_count\}\}/g, String(highCount));
template = template.replace(/\{\{medium_confidence_count\}\}/g, String(mediumCount));
template = template.replace(/\{\{low_confidence_count\}\}/g, String(lowCount));
template = template.replace(/\{\{sensitive_excluded_summary\}\}/g,
redactedCount > 0 ? `${redactedCount} pattern(s) redacted` : 'None detected');
for (const dimKey of DIMENSION_KEYS) {
const dim = analysis.dimensions[dimKey] || {};
const rating = dim.rating || 'UNSCORED';
const confidence = dim.confidence || 'UNSCORED';
const instruction = dim.claude_instruction || 'No strong preference detected. Ask the developer when this dimension is relevant.';
const summary = dim.summary || '';
let evidenceBlock = '';
if (dim.evidence && Array.isArray(dim.evidence) && dim.evidence.length > 0) {
const evidenceLines = dim.evidence.map(ev => {
const signal = ev.signal || ev.pattern || '';
const quote = ev.quote || ev.example || '';
const project = ev.project || 'unknown';
return `- **Signal:** ${signal} / **Example:** "${quote}" -- project: ${project}`;
});
evidenceBlock = evidenceLines.join('\n');
} else {
evidenceBlock = '- No evidence collected for this dimension.';
}
template = template.replace(new RegExp(`\\{\\{${dimKey}\\.rating\\}\\}`, 'g'), rating);
template = template.replace(new RegExp(`\\{\\{${dimKey}\\.confidence\\}\\}`, 'g'), confidence);
template = template.replace(new RegExp(`\\{\\{${dimKey}\\.claude_instruction\\}\\}`, 'g'), instruction);
template = template.replace(new RegExp(`\\{\\{${dimKey}\\.summary\\}\\}`, 'g'), summary);
template = template.replace(new RegExp(`\\{\\{${dimKey}\\.evidence\\}\\}`, 'g'), evidenceBlock);
}
let outputPath = options.output;
if (!outputPath) {
outputPath = path.join(os.homedir(), '.claude', 'get-shit-done', 'USER-PROFILE.md');
} else if (!path.isAbsolute(outputPath)) {
outputPath = path.join(cwd, outputPath);
}
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, template, 'utf-8');
const result = {
profile_path: outputPath,
dimensions_scored: dimensionsScored,
high_confidence: highCount,
medium_confidence: mediumCount,
low_confidence: lowCount,
sensitive_redacted: redactedCount,
source: analysis.data_source || 'session_analysis',
};
output(result, raw);
}
function cmdProfileQuestionnaire(options, raw) {
if (!options.answers) {
const questionsOutput = {
mode: 'interactive',
questions: PROFILING_QUESTIONS.map(q => ({
dimension: q.dimension,
header: q.header,
context: q.context,
question: q.question,
options: q.options.map(o => ({ label: o.label, value: o.value })),
})),
};
output(questionsOutput, raw);
return;
}
const answerValues = options.answers.split(',').map(a => a.trim());
if (answerValues.length !== PROFILING_QUESTIONS.length) {
error(`Expected ${PROFILING_QUESTIONS.length} answers (comma-separated), got ${answerValues.length}`);
}
const analysis = {
profile_version: '1.0',
analyzed_at: new Date().toISOString(),
data_source: 'questionnaire',
projects_analyzed: [],
messages_analyzed: 0,
message_threshold: 'questionnaire',
sensitive_excluded: [],
dimensions: {},
};
for (let i = 0; i < PROFILING_QUESTIONS.length; i++) {
const question = PROFILING_QUESTIONS[i];
const answerValue = answerValues[i];
const selectedOption = question.options.find(o => o.value === answerValue);
if (!selectedOption) {
error(`Invalid answer "${answerValue}" for ${question.dimension}. Valid values: ${question.options.map(o => o.value).join(', ')}`);
}
const ambiguous = isAmbiguousAnswer(question.dimension, answerValue);
analysis.dimensions[question.dimension] = {
rating: selectedOption.rating,
confidence: ambiguous ? 'LOW' : 'MEDIUM',
evidence_count: 1,
cross_project_consistent: null,
evidence: [{
signal: 'Self-reported via questionnaire',
quote: selectedOption.label,
project: 'N/A (questionnaire)',
}],
summary: `Developer self-reported as ${selectedOption.rating} for ${question.header.toLowerCase()}.`,
claude_instruction: generateClaudeInstruction(question.dimension, selectedOption.rating),
};
}
output(analysis, raw);
}
function cmdGenerateDevPreferences(cwd, options, raw) {
if (!options.analysis) error('--analysis <path> is required');
let analysisPath = options.analysis;
if (!path.isAbsolute(analysisPath)) analysisPath = path.join(cwd, analysisPath);
if (!fs.existsSync(analysisPath)) error(`Analysis file not found: ${analysisPath}`);
let analysis;
try {
analysis = JSON.parse(fs.readFileSync(analysisPath, 'utf-8'));
} catch (err) {
error(`Failed to parse analysis JSON: ${err.message}`);
}
if (!analysis.dimensions || typeof analysis.dimensions !== 'object') {
error('Analysis JSON must contain a "dimensions" object');
}
const devPrefLabels = {
communication_style: 'Communication',
decision_speed: 'Decision Support',
explanation_depth: 'Explanations',
debugging_approach: 'Debugging',
ux_philosophy: 'UX Approach',
vendor_philosophy: 'Library & Tool Choices',
frustration_triggers: 'Boundaries',
learning_style: 'Learning Support',
};
const templatePath = path.join(__dirname, '..', 'templates', 'dev-preferences.md');
if (!fs.existsSync(templatePath)) error(`Template not found: ${templatePath}`);
let template = fs.readFileSync(templatePath, 'utf-8');
const directiveLines = [];
const dimensionsIncluded = [];
for (const dimKey of DIMENSION_KEYS) {
const dim = analysis.dimensions[dimKey];
if (!dim) continue;
const label = devPrefLabels[dimKey] || dimKey;
const confidence = dim.confidence || 'UNSCORED';
let instruction = dim.claude_instruction;
if (!instruction) {
const lookup = CLAUDE_INSTRUCTIONS[dimKey];
if (lookup && dim.rating && lookup[dim.rating]) {
instruction = lookup[dim.rating];
} else {
instruction = `Adapt to this developer's ${dimKey.replace(/_/g, ' ')} preference.`;
}
}
directiveLines.push(`### ${label}\n${instruction} (${confidence} confidence)\n`);
dimensionsIncluded.push(dimKey);
}
const directivesBlock = directiveLines.join('\n').trim();
template = template.replace(/\{\{behavioral_directives\}\}/g, directivesBlock);
template = template.replace(/\{\{generated_at\}\}/g, new Date().toISOString());
template = template.replace(/\{\{data_source\}\}/g, analysis.data_source || 'session_analysis');
let stackBlock;
if (analysis.data_source === 'questionnaire') {
stackBlock = 'Stack preferences not available (questionnaire-only profile). Run `/gsd:profile-user --refresh` with session data to populate.';
} else if (options.stack) {
stackBlock = options.stack;
} else {
stackBlock = 'Stack preferences will be populated from session analysis.';
}
template = template.replace(/\{\{stack_preferences\}\}/g, stackBlock);
let outputPath = options.output;
if (!outputPath) {
outputPath = path.join(os.homedir(), '.claude', 'commands', 'gsd', 'dev-preferences.md');
} else if (!path.isAbsolute(outputPath)) {
outputPath = path.join(cwd, outputPath);
}
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, template, 'utf-8');
const result = {
command_path: outputPath,
command_name: '/gsd:dev-preferences',
dimensions_included: dimensionsIncluded,
source: analysis.data_source || 'session_analysis',
};
output(result, raw);
}
function cmdGenerateClaudeProfile(cwd, options, raw) {
if (!options.analysis) error('--analysis <path> is required');
let analysisPath = options.analysis;
if (!path.isAbsolute(analysisPath)) analysisPath = path.join(cwd, analysisPath);
if (!fs.existsSync(analysisPath)) error(`Analysis file not found: ${analysisPath}`);
let analysis;
try {
analysis = JSON.parse(fs.readFileSync(analysisPath, 'utf-8'));
} catch (err) {
error(`Failed to parse analysis JSON: ${err.message}`);
}
if (!analysis.dimensions || typeof analysis.dimensions !== 'object') {
error('Analysis JSON must contain a "dimensions" object');
}
const profileLabels = {
communication_style: 'Communication',
decision_speed: 'Decisions',
explanation_depth: 'Explanations',
debugging_approach: 'Debugging',
ux_philosophy: 'UX Philosophy',
vendor_philosophy: 'Vendor Choices',
frustration_triggers: 'Frustrations',
learning_style: 'Learning',
};
const dataSource = analysis.data_source || 'session_analysis';
const tableRows = [];
const directiveLines = [];
const dimensionsIncluded = [];
for (const dimKey of DIMENSION_KEYS) {
const dim = analysis.dimensions[dimKey];
if (!dim) continue;
const label = profileLabels[dimKey] || dimKey;
const rating = dim.rating || 'UNSCORED';
const confidence = dim.confidence || 'UNSCORED';
tableRows.push(`| ${label} | ${rating} | ${confidence} |`);
let instruction = dim.claude_instruction;
if (!instruction) {
const lookup = CLAUDE_INSTRUCTIONS[dimKey];
if (lookup && dim.rating && lookup[dim.rating]) {
instruction = lookup[dim.rating];
} else {
instruction = `Adapt to this developer's ${dimKey.replace(/_/g, ' ')} preference.`;
}
}
directiveLines.push(`- **${label}:** ${instruction}`);
dimensionsIncluded.push(dimKey);
}
const sectionLines = [
'<!-- GSD:profile-start -->',
'## Developer Profile',
'',
`> Generated by GSD from ${dataSource}. Run \`/gsd:profile-user --refresh\` to update.`,
'',
'| Dimension | Rating | Confidence |',
'|-----------|--------|------------|',
...tableRows,
'',
'**Directives:**',
...directiveLines,
'<!-- GSD:profile-end -->',
];
const sectionContent = sectionLines.join('\n');
let targetPath;
if (options.global) {
targetPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
} else if (options.output) {
targetPath = path.isAbsolute(options.output) ? options.output : path.join(cwd, options.output);
} else {
targetPath = path.join(cwd, 'CLAUDE.md');
}
let action;
if (fs.existsSync(targetPath)) {
let existingContent = fs.readFileSync(targetPath, 'utf-8');
const startMarker = '<!-- GSD:profile-start -->';
const endMarker = '<!-- GSD:profile-end -->';
const startIdx = existingContent.indexOf(startMarker);
const endIdx = existingContent.indexOf(endMarker);
if (startIdx !== -1 && endIdx !== -1) {
const before = existingContent.substring(0, startIdx);
const after = existingContent.substring(endIdx + endMarker.length);
existingContent = before + sectionContent + after;
action = 'updated';
} else {
existingContent = existingContent.trimEnd() + '\n\n' + sectionContent + '\n';
action = 'appended';
}
fs.writeFileSync(targetPath, existingContent, 'utf-8');
} else {
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, sectionContent + '\n', 'utf-8');
action = 'created';
}
const result = {
claude_md_path: targetPath,
action,
dimensions_included: dimensionsIncluded,
is_global: !!options.global,
};
output(result, raw);
}
function cmdGenerateClaudeMd(cwd, options, raw) {
const MANAGED_SECTIONS = ['project', 'stack', 'conventions', 'architecture'];
const generators = {
project: generateProjectSection,
stack: generateStackSection,
conventions: generateConventionsSection,
architecture: generateArchitectureSection,
};
const sectionHeadings = {
project: '## Project',
stack: '## Technology Stack',
conventions: '## Conventions',
architecture: '## Architecture',
};
const generated = {};
const sectionsGenerated = [];
const sectionsFallback = [];
const sectionsSkipped = [];
for (const name of MANAGED_SECTIONS) {
const gen = generators[name](cwd);
generated[name] = gen;
if (gen.hasFallback) {
sectionsFallback.push(name);
} else {
sectionsGenerated.push(name);
}
}
let outputPath = options.output;
if (!outputPath) {
outputPath = path.join(cwd, 'CLAUDE.md');
} else if (!path.isAbsolute(outputPath)) {
outputPath = path.join(cwd, outputPath);
}
let existingContent = safeReadFile(outputPath);
let action;
if (existingContent === null) {
const sections = [];
for (const name of MANAGED_SECTIONS) {
const gen = generated[name];
const heading = sectionHeadings[name];
const body = `${heading}\n\n${gen.content}`;
sections.push(buildSection(name, gen.source, body));
}
sections.push('');
sections.push(CLAUDE_MD_PROFILE_PLACEHOLDER);
existingContent = sections.join('\n\n') + '\n';
action = 'created';
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, existingContent, 'utf-8');
} else {
action = 'updated';
let fileContent = existingContent;
for (const name of MANAGED_SECTIONS) {
const gen = generated[name];
const heading = sectionHeadings[name];
const body = `${heading}\n\n${gen.content}`;
const fullSection = buildSection(name, gen.source, body);
const hasMarkers = fileContent.indexOf(`<!-- GSD:${name}-start`) !== -1;
if (hasMarkers) {
if (options.auto) {
const expectedBody = `${heading}\n\n${gen.content}`;
if (detectManualEdit(fileContent, name, expectedBody)) {
sectionsSkipped.push(name);
const genIdx = sectionsGenerated.indexOf(name);
if (genIdx !== -1) sectionsGenerated.splice(genIdx, 1);
const fbIdx = sectionsFallback.indexOf(name);
if (fbIdx !== -1) sectionsFallback.splice(fbIdx, 1);
continue;
}
}
const result = updateSection(fileContent, name, fullSection);
fileContent = result.content;
} else {
const result = updateSection(fileContent, name, fullSection);
fileContent = result.content;
}
}
if (!options.auto && fileContent.indexOf('<!-- GSD:profile-start') === -1) {
fileContent = fileContent.trimEnd() + '\n\n' + CLAUDE_MD_PROFILE_PLACEHOLDER + '\n';
}
fs.writeFileSync(outputPath, fileContent, 'utf-8');
}
const finalContent = safeReadFile(outputPath);
let profileStatus;
if (finalContent && finalContent.indexOf('<!-- GSD:profile-start') !== -1) {
if (action === 'created' || existingContent.indexOf('<!-- GSD:profile-start') === -1) {
profileStatus = 'placeholder_added';
} else {
profileStatus = 'exists';
}
} else {
profileStatus = 'already_present';
}
const genCount = sectionsGenerated.length;
const totalManaged = MANAGED_SECTIONS.length;
let message = `Generated ${genCount}/${totalManaged} sections.`;
if (sectionsFallback.length > 0) message += ` Fallback: ${sectionsFallback.join(', ')}.`;
if (sectionsSkipped.length > 0) message += ` Skipped (manually edited): ${sectionsSkipped.join(', ')}.`;
if (profileStatus === 'placeholder_added') message += ' Run /gsd:profile-user to unlock Developer Profile.';
const result = {
claude_md_path: outputPath,
action,
sections_generated: sectionsGenerated,
sections_fallback: sectionsFallback,
sections_skipped: sectionsSkipped,
sections_total: totalManaged,
profile_status: profileStatus,
message,
};
output(result, raw);
}
module.exports = {
cmdWriteProfile,
cmdProfileQuestionnaire,
cmdGenerateDevPreferences,
cmdGenerateClaudeProfile,
cmdGenerateClaudeMd,
PROFILING_QUESTIONS,
CLAUDE_INSTRUCTIONS,
};

View File

@@ -0,0 +1,537 @@
/**
* Profile Pipeline — session scanning, message extraction, and sampling
*
* Reads Claude Code session history (read-only) to extract user messages
* for behavioral profiling. Three commands:
* - scan-sessions: list all projects and sessions
* - extract-messages: extract user messages from a specific project
* - profile-sample: multi-project sampling with recency weighting
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const readline = require('readline');
const { output, error, safeReadFile } = require('./core.cjs');
// ─── Session I/O Helpers ──────────────────────────────────────────────────────
function getSessionsDir(overridePath) {
const dir = overridePath || path.join(os.homedir(), '.claude', 'projects');
if (!fs.existsSync(dir)) return null;
return dir;
}
function scanProjectDir(projectDirPath) {
const entries = fs.readdirSync(projectDirPath);
const sessions = [];
for (const entry of entries) {
if (!entry.endsWith('.jsonl')) continue;
const sessionId = entry.replace('.jsonl', '');
const filePath = path.join(projectDirPath, entry);
const stat = fs.statSync(filePath);
sessions.push({
sessionId,
filePath,
size: stat.size,
modified: stat.mtime,
});
}
sessions.sort((a, b) => b.modified - a.modified);
return sessions;
}
function readSessionIndex(projectDirPath) {
try {
const indexPath = path.join(projectDirPath, 'sessions-index.json');
const raw = fs.readFileSync(indexPath, 'utf-8');
const parsed = JSON.parse(raw);
const entries = new Map();
for (const entry of (parsed.entries || [])) {
if (entry.sessionId) {
entries.set(entry.sessionId, entry);
}
}
return { originalPath: parsed.originalPath || null, entries };
} catch {
return { originalPath: null, entries: new Map() };
}
}
function getProjectName(projectDirName, indexData, firstRecordCwd) {
if (indexData && indexData.originalPath) {
return path.basename(indexData.originalPath);
}
if (firstRecordCwd) {
return path.basename(firstRecordCwd);
}
return projectDirName;
}
function formatBytes(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1073741824) return `${(bytes / 1048576).toFixed(1)} MB`;
return `${(bytes / 1073741824).toFixed(1)} GB`;
}
function formatProjectTable(projects) {
let out = '';
out += 'Project'.padEnd(35) + 'Sessions'.padEnd(10) + 'Size'.padEnd(10) + 'Last Active\n';
out += '-'.repeat(75) + '\n';
for (const p of projects) {
const name = p.name.length > 33 ? p.name.substring(0, 30) + '...' : p.name;
out += name.padEnd(35) + String(p.sessionCount).padEnd(10) +
p.totalSizeHuman.padEnd(10) + p.lastActive + '\n';
}
return out;
}
function formatSessionTable(sessions) {
let out = '';
out += ' Session ID'.padEnd(42) + 'Size'.padEnd(10) + 'Modified\n';
out += ' ' + '-'.repeat(70) + '\n';
for (const s of sessions) {
const id = s.sessionId.length > 38 ? s.sessionId.substring(0, 35) + '...' : s.sessionId;
out += ' ' + id.padEnd(40) + formatBytes(s.size).padEnd(10) +
new Date(s.modified).toISOString().replace('T', ' ').substring(0, 19) + '\n';
}
return out;
}
// ─── Message Extraction Helpers ───────────────────────────────────────────────
function isGenuineUserMessage(record) {
if (record.type !== 'user') return false;
if (record.userType !== 'external') return false;
if (record.isMeta === true) return false;
if (record.isSidechain === true) return false;
const content = record.message?.content;
if (typeof content !== 'string') return false;
if (content.length === 0) return false;
if (content.startsWith('<local-command')) return false;
if (content.startsWith('<command-')) return false;
if (content.startsWith('<task-notification')) return false;
if (content.startsWith('<local-command-stdout')) return false;
return true;
}
function truncateContent(content, maxLen = 2000) {
if (content.length <= maxLen) return content;
return content.substring(0, maxLen) + '... [truncated]';
}
async function streamExtractMessages(filePath, filterFn, maxMessages = 300) {
const rl = readline.createInterface({
input: fs.createReadStream(filePath),
crlfDelay: Infinity,
terminal: false,
});
const messages = [];
const sessionId = path.basename(filePath, '.jsonl');
for await (const line of rl) {
if (messages.length >= maxMessages) break;
let record;
try {
record = JSON.parse(line);
} catch {
continue;
}
if (!filterFn(record)) continue;
messages.push({
sessionId,
projectPath: record.cwd || null,
timestamp: record.timestamp || null,
content: truncateContent(record.message.content),
});
}
return messages;
}
// ─── Commands ─────────────────────────────────────────────────────────────────
async function cmdScanSessions(overridePath, options, raw) {
const sessionsDir = getSessionsDir(overridePath);
if (!sessionsDir) {
const searchedPath = overridePath || '~/.claude/projects';
error(`No Claude Code sessions found at ${searchedPath}.${overridePath ? '' : ' Is Claude Code installed?'}`);
}
process.stderr.write('Reading your session history (read-only, nothing is modified or sent anywhere)...\n');
let projectDirs;
try {
projectDirs = fs.readdirSync(sessionsDir).filter(entry => {
const fullPath = path.join(sessionsDir, entry);
try {
return fs.statSync(fullPath).isDirectory();
} catch {
return false;
}
});
} catch (err) {
error(`Cannot read sessions directory: ${err.message}`);
}
const projects = [];
for (const dirName of projectDirs) {
const projectPath = path.join(sessionsDir, dirName);
const sessions = scanProjectDir(projectPath);
if (sessions.length === 0) continue;
const indexData = readSessionIndex(projectPath);
const projectName = getProjectName(dirName, indexData);
if (indexData.entries.size === 0 && !options.json) {
process.stderr.write(`Index not found for ${projectName}, scanning directory...\n`);
}
const totalSize = sessions.reduce((sum, s) => sum + s.size, 0);
const lastActive = sessions[0].modified.toISOString();
const oldest = sessions[sessions.length - 1].modified.toISOString();
const newest = sessions[0].modified.toISOString();
const project = {
name: projectName,
directory: dirName,
sessionCount: sessions.length,
totalSize,
totalSizeHuman: formatBytes(totalSize),
lastActive: lastActive.replace('T', ' ').substring(0, 19),
dateRange: { first: oldest, last: newest },
};
if (options.verbose) {
project.sessions = sessions.map(s => {
const indexed = indexData.entries.get(s.sessionId);
const session = {
sessionId: s.sessionId,
size: s.size,
sizeHuman: formatBytes(s.size),
modified: s.modified.toISOString(),
};
if (indexed) {
if (indexed.summary) session.summary = indexed.summary;
if (indexed.messageCount !== undefined) session.messageCount = indexed.messageCount;
if (indexed.created) session.created = indexed.created;
}
return session;
});
}
projects.push(project);
}
projects.sort((a, b) => b.dateRange.last.localeCompare(a.dateRange.last));
if (options.json || raw) {
output(projects, raw);
} else {
process.stdout.write('\n' + formatProjectTable(projects));
if (options.verbose) {
for (const p of projects) {
process.stdout.write(`\n ${p.name} (${p.sessionCount} sessions):\n`);
if (p.sessions) {
process.stdout.write(formatSessionTable(p.sessions));
}
}
}
process.stdout.write(`\nTotal: ${projects.length} projects\n`);
process.exit(0);
}
}
async function cmdExtractMessages(projectArg, options, raw, overridePath) {
const sessionsDir = getSessionsDir(overridePath);
if (!sessionsDir) {
const searchedPath = overridePath || '~/.claude/projects';
error(`No Claude Code sessions found at ${searchedPath}.${overridePath ? '' : ' Is Claude Code installed?'}`);
}
let projectDirs;
try {
projectDirs = fs.readdirSync(sessionsDir).filter(entry => {
const fullPath = path.join(sessionsDir, entry);
try {
return fs.statSync(fullPath).isDirectory();
} catch {
return false;
}
});
} catch (err) {
error(`Cannot read sessions directory: ${err.message}`);
}
let matchedDir = null;
let matchedName = null;
for (const dirName of projectDirs) {
if (dirName === projectArg) {
matchedDir = dirName;
break;
}
}
if (!matchedDir) {
const lowerArg = projectArg.toLowerCase();
const matches = projectDirs.filter(d => d.toLowerCase().includes(lowerArg));
if (matches.length === 1) {
matchedDir = matches[0];
} else if (matches.length > 1) {
const exactNameMatches = [];
for (const dirName of matches) {
const indexData = readSessionIndex(path.join(sessionsDir, dirName));
const pName = getProjectName(dirName, indexData);
if (pName.toLowerCase() === lowerArg) {
exactNameMatches.push({ dirName, name: pName });
}
}
if (exactNameMatches.length === 1) {
matchedDir = exactNameMatches[0].dirName;
matchedName = exactNameMatches[0].name;
} else {
const names = matches.map(d => {
const idx = readSessionIndex(path.join(sessionsDir, d));
return ` - ${getProjectName(d, idx)} (${d})`;
});
error(`Multiple projects match "${projectArg}":\n${names.join('\n')}\nBe more specific.`);
}
}
}
if (!matchedDir) {
const available = projectDirs.map(d => {
const idx = readSessionIndex(path.join(sessionsDir, d));
return ` - ${getProjectName(d, idx)}`;
});
error(`No project matching "${projectArg}". Available projects:\n${available.join('\n')}`);
}
const projectPath = path.join(sessionsDir, matchedDir);
const indexData = readSessionIndex(projectPath);
const projectName = matchedName || getProjectName(matchedDir, indexData);
process.stderr.write('Reading your session history (read-only, nothing is modified or sent anywhere)...\n');
let sessions = scanProjectDir(projectPath);
if (options.sessionId) {
sessions = sessions.filter(s => s.sessionId === options.sessionId);
if (sessions.length === 0) {
error(`Session "${options.sessionId}" not found in project "${projectName}".`);
}
}
if (options.limit && options.limit > 0) {
sessions = sessions.slice(0, options.limit);
}
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-pipeline-'));
const outputPath = path.join(tmpDir, 'extracted-messages.jsonl');
let sessionsProcessed = 0;
let sessionsSkipped = 0;
let messagesExtracted = 0;
let messagesTruncated = 0;
const total = sessions.length;
const batchLimit = 300;
for (let i = 0; i < sessions.length; i++) {
if (messagesExtracted >= batchLimit) break;
const session = sessions[i];
process.stderr.write(`\rProcessing session ${i + 1}/${total}...`);
try {
const remaining = batchLimit - messagesExtracted;
const msgs = await streamExtractMessages(session.filePath, isGenuineUserMessage, remaining);
for (const msg of msgs) {
fs.appendFileSync(outputPath, JSON.stringify(msg) + '\n');
messagesExtracted++;
if (msg.content.endsWith('... [truncated]')) {
messagesTruncated++;
}
}
sessionsProcessed++;
} catch (err) {
sessionsSkipped++;
process.stderr.write(`\nWarning: Skipped session ${session.sessionId}: ${err.message}\n`);
}
}
process.stderr.write('\r' + ' '.repeat(60) + '\r');
const result = {
output_file: outputPath,
project: projectName,
sessions_processed: sessionsProcessed,
sessions_skipped: sessionsSkipped,
messages_extracted: messagesExtracted,
messages_truncated: messagesTruncated,
};
if (sessionsSkipped > 0 && sessionsProcessed > 0) {
process.stdout.write(JSON.stringify(result, null, 2));
process.exit(2);
} else if (sessionsProcessed === 0 && sessionsSkipped > 0) {
process.stdout.write(JSON.stringify(result, null, 2));
process.exit(1);
} else {
output(result, raw);
}
}
async function cmdProfileSample(overridePath, options, raw) {
const sessionsDir = getSessionsDir(overridePath);
if (!sessionsDir) {
const searchedPath = overridePath || '~/.claude/projects';
error(`No Claude Code sessions found at ${searchedPath}.${overridePath ? '' : ' Is Claude Code installed?'}`);
}
process.stderr.write('Reading your session history (read-only, nothing is modified or sent anywhere)...\n');
const limit = options.limit || 150;
const maxChars = options.maxChars || 500;
let projectDirs;
try {
projectDirs = fs.readdirSync(sessionsDir).filter(entry => {
const fullPath = path.join(sessionsDir, entry);
try {
return fs.statSync(fullPath).isDirectory();
} catch {
return false;
}
});
} catch (err) {
error(`Cannot read sessions directory: ${err.message}`);
}
if (projectDirs.length === 0) {
error('No project directories found in sessions directory.');
}
const projectMeta = [];
for (const dirName of projectDirs) {
const projectPath = path.join(sessionsDir, dirName);
const sessions = scanProjectDir(projectPath);
if (sessions.length === 0) continue;
const indexData = readSessionIndex(projectPath);
const projectName = getProjectName(dirName, indexData);
const lastActive = sessions[0].modified;
projectMeta.push({ dirName, projectPath, sessions, projectName, lastActive });
}
projectMeta.sort((a, b) => b.lastActive - a.lastActive);
const projectCount = projectMeta.length;
if (projectCount === 0) {
error('No projects with sessions found.');
}
const perProjectCap = options.maxPerProject || Math.max(5, Math.floor(limit / projectCount));
const recencyThreshold = Date.now() - 30 * 24 * 60 * 60 * 1000;
const allMessages = [];
let skippedContextDumps = 0;
const projectBreakdown = [];
for (const proj of projectMeta) {
if (allMessages.length >= limit) break;
const cappedSessions = proj.sessions.slice(0, perProjectCap);
let projectMessages = 0;
let projectSessionsUsed = 0;
for (const session of cappedSessions) {
if (allMessages.length >= limit) break;
const isRecent = session.modified.getTime() >= recencyThreshold;
const perSessionMax = isRecent ? 10 : 3;
const remaining = Math.min(perSessionMax, limit - allMessages.length);
try {
const msgs = await streamExtractMessages(session.filePath, isGenuineUserMessage, remaining);
let sessionUsed = false;
for (const msg of msgs) {
if (allMessages.length >= limit) break;
const content = msg.content || '';
if (content.startsWith('This session is being continued')) {
skippedContextDumps++;
continue;
}
const lines = content.split('\n').filter(l => l.trim().length > 0);
if (lines.length > 3) {
const logPattern = /^\[?(DEBUG|INFO|WARN|ERROR|LOG)\]?/i;
const timestampPattern = /^\d{4}-\d{2}-\d{2}/;
const logLines = lines.filter(l => logPattern.test(l.trim()) || timestampPattern.test(l.trim()));
if (logLines.length / lines.length > 0.8) {
skippedContextDumps++;
continue;
}
}
const truncated = truncateContent(content, maxChars);
allMessages.push({
sessionId: msg.sessionId,
projectName: proj.projectName,
projectPath: msg.projectPath,
timestamp: msg.timestamp,
content: truncated,
});
projectMessages++;
sessionUsed = true;
}
if (sessionUsed) projectSessionsUsed++;
} catch {
continue;
}
}
if (projectMessages > 0) {
projectBreakdown.push({
project: proj.projectName,
messages: projectMessages,
sessions: projectSessionsUsed,
});
}
}
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-'));
const outputPath = path.join(tmpDir, 'profile-sample.jsonl');
for (const msg of allMessages) {
fs.appendFileSync(outputPath, JSON.stringify(msg) + '\n');
}
const result = {
output_file: outputPath,
projects_sampled: projectBreakdown.length,
messages_sampled: allMessages.length,
per_project_cap: perProjectCap,
message_char_limit: maxChars,
skipped_context_dumps: skippedContextDumps,
project_breakdown: projectBreakdown,
};
output(result, raw);
}
module.exports = {
cmdScanSessions,
cmdExtractMessages,
cmdProfileSample,
};

View File

@@ -0,0 +1,681 @@
# User Profiling: Detection Heuristics Reference
This reference document defines detection heuristics for behavioral profiling across 8 dimensions. The gsd-user-profiler agent applies these rules when analyzing extracted session messages. Do not invent dimensions or scoring rules beyond what is defined here.
## How to Use This Document
1. The gsd-user-profiler agent reads this document before analyzing any messages
2. For each dimension, the agent scans messages for the signal patterns defined below
3. The agent applies the detection heuristics to classify the developer's pattern
4. Confidence is scored using the thresholds defined per dimension
5. Evidence quotes are curated using the rules in the Evidence Curation section
6. Output must conform to the JSON schema in the Output Schema section
---
## Dimensions
### 1. Communication Style
`dimension_id: communication_style`
**What we're measuring:** How the developer phrases requests, instructions, and feedback -- the structural pattern of their messages to Claude.
**Rating spectrum:**
| Rating | Description |
|--------|-------------|
| `terse-direct` | Short, imperative messages with minimal context. Gets to the point immediately. |
| `conversational` | Medium-length messages mixing instructions with questions and thinking-aloud. Natural, informal tone. |
| `detailed-structured` | Long messages with explicit structure -- headers, numbered lists, problem statements, pre-analysis. |
| `mixed` | No dominant pattern; style shifts based on task type or project context. |
**Signal patterns:**
1. **Message length distribution** -- Average word count across messages. Terse < 50 words, conversational 50-200 words, detailed > 200 words.
2. **Imperative-to-interrogative ratio** -- Ratio of commands ("fix this", "add X") to questions ("what do you think?", "should we?"). High imperative ratio suggests terse-direct.
3. **Structural formatting** -- Presence of markdown headers, numbered lists, code blocks, or bullet points within messages. Frequent formatting suggests detailed-structured.
4. **Context preambles** -- Whether the developer provides background/context before making a request. Preambles suggest conversational or detailed-structured.
5. **Sentence completeness** -- Whether messages use full sentences or fragments/shorthand. Fragments suggest terse-direct.
6. **Follow-up pattern** -- Whether the developer provides additional context in subsequent messages (multi-message requests suggest conversational).
**Detection heuristics:**
1. If average message length < 50 words AND predominantly imperative mood AND minimal formatting --> `terse-direct`
2. If average message length 50-200 words AND mix of imperative and interrogative AND occasional formatting --> `conversational`
3. If average message length > 200 words AND frequent structural formatting AND context preambles present --> `detailed-structured`
4. If message length variance is high (std dev > 60% of mean) AND no single pattern dominates (< 60% of messages match one style) --> `mixed`
5. If pattern varies systematically by project type (e.g., terse in CLI projects, detailed in frontend) --> `mixed` with context-dependent note
**Confidence scoring:**
- **HIGH:** 10+ messages showing consistent pattern (> 70% match), same pattern observed across 2+ projects
- **MEDIUM:** 5-9 messages showing pattern, OR pattern consistent within 1 project only
- **LOW:** < 5 messages with relevant signals, OR mixed signals (contradictory patterns observed in similar contexts)
- **UNSCORED:** 0 messages with relevant signals for this dimension
**Example quotes:**
- **terse-direct:** "fix the auth bug" / "add pagination to the list endpoint" / "this test is failing, make it pass"
- **conversational:** "I'm thinking we should probably handle the error case here. What do you think about returning a 422 instead of a 500? The client needs to know it was a validation issue."
- **detailed-structured:** "## Context\nThe auth flow currently uses session cookies but we need to migrate to JWT.\n\n## Requirements\n1. Access tokens (15min expiry)\n2. Refresh tokens (7-day)\n3. httpOnly cookies\n\n## What I've tried\nI looked at jose and jsonwebtoken..."
**Context-dependent patterns:**
When communication style varies systematically by project or task type, report the split rather than forcing a single rating. Example: "context-dependent: terse-direct for bug fixes and CLI tooling, detailed-structured for architecture and frontend work." Phase 3 orchestration resolves context-dependent splits by presenting the split to the user.
---
### 2. Decision Speed
`dimension_id: decision_speed`
**What we're measuring:** How quickly the developer makes choices when Claude presents options, alternatives, or trade-offs.
**Rating spectrum:**
| Rating | Description |
|--------|-------------|
| `fast-intuitive` | Decides immediately based on experience or gut feeling. Minimal deliberation. |
| `deliberate-informed` | Requests comparison or summary before deciding. Wants to understand trade-offs. |
| `research-first` | Delays decision to research independently. May leave and return with findings. |
| `delegator` | Defers to Claude's recommendation. Trusts the suggestion. |
**Signal patterns:**
1. **Response latency to options** -- How many messages between Claude presenting options and developer choosing. Immediate (same message or next) suggests fast-intuitive.
2. **Comparison requests** -- Presence of "compare these", "what are the trade-offs?", "pros and cons?" suggests deliberate-informed.
3. **External research indicators** -- Messages like "I looked into X and...", "according to the docs...", "I read that..." suggest research-first.
4. **Delegation language** -- "just pick one", "whatever you recommend", "your call", "go with the best option" suggests delegator.
5. **Decision reversal frequency** -- How often the developer changes a decision after making it. Frequent reversals may indicate fast-intuitive with low confidence.
**Detection heuristics:**
1. If developer selects options within 1-2 messages of presentation AND uses decisive language ("use X", "go with A") AND rarely asks for comparisons --> `fast-intuitive`
2. If developer requests trade-off analysis or comparison tables AND decides after receiving comparison AND asks clarifying questions --> `deliberate-informed`
3. If developer defers decisions with "let me look into this" AND returns with external information AND cites documentation or articles --> `research-first`
4. If developer uses delegation language (> 3 instances) AND rarely overrides Claude's choices AND says "sounds good" or "your call" --> `delegator`
5. If no clear pattern OR evidence is split across multiple styles --> classify as the dominant style with a context-dependent note
**Confidence scoring:**
- **HIGH:** 10+ decision points observed showing consistent pattern, same pattern across 2+ projects
- **MEDIUM:** 5-9 decision points, OR consistent within 1 project only
- **LOW:** < 5 decision points observed, OR mixed decision-making styles
- **UNSCORED:** 0 messages containing decision-relevant signals
**Example quotes:**
- **fast-intuitive:** "Use Tailwind. Next question." / "Option B, let's move on"
- **deliberate-informed:** "Can you compare Prisma vs Drizzle for this use case? I want to understand the migration story and type safety differences before I pick."
- **research-first:** "Hold off on the DB choice -- I want to read the Drizzle docs and check their GitHub issues first. I'll come back with a decision."
- **delegator:** "You know more about this than me. Whatever you recommend, go with it."
**Context-dependent patterns:**
Decision speed often varies by stakes. A developer may be fast-intuitive for styling choices but research-first for database or auth decisions. When this pattern is clear, report the split: "context-dependent: fast-intuitive for low-stakes (styling, naming), deliberate-informed for high-stakes (architecture, security)."
---
### 3. Explanation Depth
`dimension_id: explanation_depth`
**What we're measuring:** How much explanation the developer wants alongside code -- their preference for understanding vs. speed.
**Rating spectrum:**
| Rating | Description |
|--------|-------------|
| `code-only` | Wants working code with minimal or no explanation. Reads and understands code directly. |
| `concise` | Wants brief explanation of approach with code. Key decisions noted, not exhaustive. |
| `detailed` | Wants thorough walkthrough of the approach, reasoning, and code. Appreciates structure. |
| `educational` | Wants deep conceptual explanation. Treats interactions as learning opportunities. |
**Signal patterns:**
1. **Explicit depth requests** -- "just show me the code", "explain why", "teach me about X", "skip the explanation"
2. **Reaction to explanations** -- Does the developer skip past explanations? Ask for more detail? Say "too much"?
3. **Follow-up question depth** -- Surface-level follow-ups ("does it work?") vs. conceptual ("why this pattern over X?")
4. **Code comprehension signals** -- Does the developer reference implementation details in their messages? This suggests they read and understand code directly.
5. **"I know this" signals** -- Messages like "I'm familiar with X", "skip the basics", "I know how hooks work" indicate lower explanation preference.
**Detection heuristics:**
1. If developer says "just the code" or "skip the explanation" AND rarely asks follow-up conceptual questions AND references code details directly --> `code-only`
2. If developer accepts brief explanations without asking for more AND asks focused follow-ups about specific decisions --> `concise`
3. If developer asks "why" questions AND requests walkthroughs AND appreciates structured explanations --> `detailed`
4. If developer asks conceptual questions beyond the immediate task AND uses learning language ("I want to understand", "teach me") --> `educational`
**Confidence scoring:**
- **HIGH:** 10+ messages showing consistent preference, same preference across 2+ projects
- **MEDIUM:** 5-9 messages, OR consistent within 1 project only
- **LOW:** < 5 relevant messages, OR preferences shift between interactions
- **UNSCORED:** 0 messages with relevant signals
**Example quotes:**
- **code-only:** "Just give me the implementation. I'll read through it." / "Skip the explanation, show the code."
- **concise:** "Quick summary of the approach, then the code please." / "Why did you use a Map here instead of an object?"
- **detailed:** "Walk me through this step by step. I want to understand the auth flow before we implement it."
- **educational:** "Can you explain how JWT refresh token rotation works conceptually? I want to understand the security model, not just implement it."
**Context-dependent patterns:**
Explanation depth often correlates with domain familiarity. A developer may want code-only for well-known tech but educational for new domains. Report splits when observed: "context-dependent: code-only for React/TypeScript, detailed for database optimization."
---
### 4. Debugging Approach
`dimension_id: debugging_approach`
**What we're measuring:** How the developer approaches problems, errors, and unexpected behavior when working with Claude.
**Rating spectrum:**
| Rating | Description |
|--------|-------------|
| `fix-first` | Pastes error, wants it fixed. Minimal diagnosis interest. Results-oriented. |
| `diagnostic` | Shares error with context, wants to understand the cause before fixing. |
| `hypothesis-driven` | Investigates independently first, brings specific theories to Claude for validation. |
| `collaborative` | Wants to work through the problem step-by-step with Claude as a partner. |
**Signal patterns:**
1. **Error presentation style** -- Raw error paste only (fix-first) vs. error + "I think it might be..." (hypothesis-driven) vs. "Can you help me understand why..." (diagnostic)
2. **Pre-investigation indicators** -- Does the developer share what they already tried? Do they mention reading logs, checking state, or isolating the issue?
3. **Root cause interest** -- After a fix, does the developer ask "why did that happen?" or just move on?
4. **Step-by-step language** -- "Let's check X first", "what should we look at next?", "walk me through the debugging"
5. **Fix acceptance pattern** -- Does the developer immediately apply fixes or question them first?
**Detection heuristics:**
1. If developer pastes errors without context AND accepts fixes without root cause questions AND moves on immediately --> `fix-first`
2. If developer provides error context AND asks "why is this happening?" AND wants explanation with the fix --> `diagnostic`
3. If developer shares their own analysis AND proposes theories ("I think the issue is X because...") AND asks Claude to confirm or refute --> `hypothesis-driven`
4. If developer uses collaborative language ("let's", "what should we check?") AND prefers incremental diagnosis AND walks through problems together --> `collaborative`
**Confidence scoring:**
- **HIGH:** 10+ debugging interactions showing consistent approach, same approach across 2+ projects
- **MEDIUM:** 5-9 debugging interactions, OR consistent within 1 project only
- **LOW:** < 5 debugging interactions, OR approach varies significantly
- **UNSCORED:** 0 messages with debugging-relevant signals
**Example quotes:**
- **fix-first:** "Getting this error: TypeError: Cannot read properties of undefined. Fix it."
- **diagnostic:** "The API returns 500 when I send a POST to /users. Here's the request body and the server log. What's causing this?"
- **hypothesis-driven:** "I think the race condition is in the useEffect cleanup. I checked and the subscription isn't being cancelled on unmount. Can you confirm?"
- **collaborative:** "Let's debug this together. The test passes locally but fails in CI. What should we check first?"
**Context-dependent patterns:**
Debugging approach may vary by urgency. A developer might be fix-first under deadline pressure but hypothesis-driven during regular development. Note temporal patterns if detected.
---
### 5. UX Philosophy
`dimension_id: ux_philosophy`
**What we're measuring:** How the developer prioritizes user experience, design, and visual quality relative to functionality.
**Rating spectrum:**
| Rating | Description |
|--------|-------------|
| `function-first` | Get it working, polish later. Minimal UX concern during implementation. |
| `pragmatic` | Basic usability from the start. Nothing ugly or broken, but no design obsession. |
| `design-conscious` | Design and UX are treated as important as functionality. Attention to visual detail. |
| `backend-focused` | Primarily builds backend/CLI. Minimal frontend exposure or interest. |
**Signal patterns:**
1. **Design-related requests** -- Mentions of styling, layout, responsiveness, animations, color schemes, spacing
2. **Polish timing** -- Does the developer ask for visual polish during implementation or defer it?
3. **UI feedback specificity** -- Vague ("make it look better") vs. specific ("increase the padding to 16px, change the font weight to 600")
4. **Frontend vs. backend distribution** -- Ratio of frontend-focused requests to backend-focused requests
5. **Accessibility mentions** -- References to a11y, screen readers, keyboard navigation, ARIA labels
**Detection heuristics:**
1. If developer rarely mentions UI/UX AND focuses on logic, APIs, data AND defers styling ("we'll make it pretty later") --> `function-first`
2. If developer includes basic UX requirements AND mentions usability but not pixel-perfection AND balances form with function --> `pragmatic`
3. If developer provides specific design requirements AND mentions polish, animations, spacing AND treats UI bugs as seriously as logic bugs --> `design-conscious`
4. If developer works primarily on CLI tools, APIs, or backend systems AND rarely or never works on frontend AND messages focus on data, performance, infrastructure --> `backend-focused`
**Confidence scoring:**
- **HIGH:** 10+ messages with UX-relevant signals, same pattern across 2+ projects
- **MEDIUM:** 5-9 messages, OR consistent within 1 project only
- **LOW:** < 5 relevant messages, OR philosophy varies by project type
- **UNSCORED:** 0 messages with UX-relevant signals
**Example quotes:**
- **function-first:** "Just get the form working. We'll style it later." / "I don't care how it looks, I need the data flowing."
- **pragmatic:** "Make sure the loading state is visible and the error messages are clear. Standard styling is fine."
- **design-conscious:** "The button needs more breathing room -- add 12px vertical padding and make the hover state transition 200ms. Also check the contrast ratio."
- **backend-focused:** "I'm building a CLI tool. No UI needed." / "Add the REST endpoint, I'll handle the frontend separately."
**Context-dependent patterns:**
UX philosophy is inherently project-dependent. A developer building a CLI tool is necessarily backend-focused for that project. When possible, distinguish between project-driven and preference-driven patterns. If the developer only has backend projects, note that the rating reflects available data: "backend-focused (note: all analyzed projects are backend/CLI -- may not reflect frontend preferences)."
---
### 6. Vendor Philosophy
`dimension_id: vendor_philosophy`
**What we're measuring:** How the developer approaches choosing and evaluating libraries, frameworks, and external services.
**Rating spectrum:**
| Rating | Description |
|--------|-------------|
| `pragmatic-fast` | Uses what works, what Claude suggests, or what's fastest. Minimal evaluation. |
| `conservative` | Prefers well-known, battle-tested, widely-adopted options. Risk-averse. |
| `thorough-evaluator` | Researches alternatives, reads docs, compares features and trade-offs before committing. |
| `opinionated` | Has strong, pre-existing preferences for specific tools. Knows what they like. |
**Signal patterns:**
1. **Library selection language** -- "just use whatever", "is X the standard?", "I want to compare A vs B", "we're using X, period"
2. **Evaluation depth** -- Does the developer accept the first suggestion or ask for alternatives?
3. **Stated preferences** -- Explicit mentions of preferred tools, past experience, or tool philosophy
4. **Rejection patterns** -- Does the developer reject Claude's suggestions? On what basis (popularity, personal experience, docs quality)?
5. **Dependency attitude** -- "minimize dependencies", "no external deps", "add whatever we need" -- reveals philosophy about external code
**Detection heuristics:**
1. If developer accepts library suggestions without pushback AND uses phrases like "sounds good" or "go with that" AND rarely asks about alternatives --> `pragmatic-fast`
2. If developer asks about popularity, maintenance, community AND prefers "industry standard" or "battle-tested" AND avoids new/experimental --> `conservative`
3. If developer requests comparisons AND reads docs before deciding AND asks about edge cases, license, bundle size --> `thorough-evaluator`
4. If developer names specific libraries unprompted AND overrides Claude's suggestions AND expresses strong preferences --> `opinionated`
**Confidence scoring:**
- **HIGH:** 10+ vendor/library decisions observed, same pattern across 2+ projects
- **MEDIUM:** 5-9 decisions, OR consistent within 1 project only
- **LOW:** < 5 vendor decisions observed, OR pattern varies
- **UNSCORED:** 0 messages with vendor-selection signals
**Example quotes:**
- **pragmatic-fast:** "Use whatever ORM you recommend. I just need it working." / "Sure, Tailwind is fine."
- **conservative:** "Is Prisma the most widely used ORM for this? I want something with a large community." / "Let's stick with what most teams use."
- **thorough-evaluator:** "Before we pick a state management library, can you compare Zustand vs Jotai vs Redux Toolkit? I want to understand bundle size, API surface, and TypeScript support."
- **opinionated:** "We're using Drizzle, not Prisma. I've used both and Drizzle's SQL-like API is better for complex queries."
**Context-dependent patterns:**
Vendor philosophy may shift based on project importance or domain. Personal projects may use pragmatic-fast while professional projects use thorough-evaluator. Report the split if detected.
---
### 7. Frustration Triggers
`dimension_id: frustration_triggers`
**What we're measuring:** What causes visible frustration, correction, or negative emotional signals in the developer's messages to Claude.
**Rating spectrum:**
| Rating | Description |
|--------|-------------|
| `scope-creep` | Frustrated when Claude does things that were not asked for. Wants bounded execution. |
| `instruction-adherence` | Frustrated when Claude doesn't follow instructions precisely. Values exactness. |
| `verbosity` | Frustrated when Claude over-explains or is too wordy. Wants conciseness. |
| `regression` | Frustrated when Claude breaks working code while fixing something else. Values stability. |
**Signal patterns:**
1. **Correction language** -- "I didn't ask for that", "don't do X", "I said Y not Z", "why did you change this?"
2. **Repetition patterns** -- Repeating the same instruction with emphasis suggests instruction-adherence frustration
3. **Emotional tone shifts** -- Shift from neutral to terse, use of capitals, exclamation marks, explicit frustration words
4. **"Don't" statements** -- "don't add extra features", "don't explain so much", "don't touch that file" -- what they prohibit reveals what frustrates them
5. **Frustration recovery** -- How quickly the developer returns to neutral tone after a frustration event
**Detection heuristics:**
1. If developer corrects Claude for doing unrequested work AND uses language like "I only asked for X", "stop adding things", "stick to what I asked" --> `scope-creep`
2. If developer repeats instructions AND corrects specific deviations from stated requirements AND emphasizes precision ("I specifically said...") --> `instruction-adherence`
3. If developer asks Claude to be shorter AND skips explanations AND expresses annoyance at length ("too much", "just the answer") --> `verbosity`
4. If developer expresses frustration at broken functionality AND checks for regressions AND says "you broke X while fixing Y" --> `regression`
**Confidence scoring:**
- **HIGH:** 10+ frustration events showing consistent trigger pattern, same trigger across 2+ projects
- **MEDIUM:** 5-9 frustration events, OR consistent within 1 project only
- **LOW:** < 5 frustration events observed (note: low frustration count is POSITIVE -- it means the developer is generally satisfied, not that data is insufficient)
- **UNSCORED:** 0 messages with frustration signals (note: "no frustration detected" is a valid finding)
**Example quotes:**
- **scope-creep:** "I asked you to fix the login bug, not refactor the entire auth module. Revert everything except the bug fix."
- **instruction-adherence:** "I said to use a Map, not an object. I was specific about this. Please redo it with a Map."
- **verbosity:** "Way too much explanation. Just show me the code change, nothing else."
- **regression:** "The search was working fine before. Now after your 'fix' to the filter, search results are empty. Don't touch things I didn't ask you to change."
**Context-dependent patterns:**
Frustration triggers tend to be consistent across projects (personality-driven, not project-driven). However, their intensity may vary with project stakes. If multiple frustration triggers are observed, report the primary (most frequent) and note secondaries.
---
### 8. Learning Style
`dimension_id: learning_style`
**What we're measuring:** How the developer prefers to understand new concepts, tools, or patterns they encounter.
**Rating spectrum:**
| Rating | Description |
|--------|-------------|
| `self-directed` | Reads code directly, figures things out independently. Asks Claude specific questions. |
| `guided` | Asks Claude to explain relevant parts. Prefers guided understanding. |
| `documentation-first` | Reads official docs and tutorials before diving in. References documentation. |
| `example-driven` | Wants working examples to modify and learn from. Pattern-matching learner. |
**Signal patterns:**
1. **Learning initiation** -- Does the developer start by reading code, asking for explanation, requesting docs, or asking for examples?
2. **Reference to external sources** -- Mentions of documentation, tutorials, Stack Overflow, blog posts suggest documentation-first
3. **Example requests** -- "show me an example", "can you give me a sample?", "let me see how this looks in practice"
4. **Code-reading indicators** -- "I looked at the implementation", "I see that X calls Y", "from reading the code..."
5. **Explanation requests vs. code requests** -- Ratio of "explain X" to "show me X" messages
**Detection heuristics:**
1. If developer references reading code directly AND asks specific targeted questions AND demonstrates independent investigation --> `self-directed`
2. If developer asks Claude to explain concepts AND requests walkthroughs AND prefers Claude-mediated understanding --> `guided`
3. If developer cites documentation AND asks for doc links AND mentions reading tutorials or official guides --> `documentation-first`
4. If developer requests examples AND modifies provided examples AND learns by pattern matching --> `example-driven`
**Confidence scoring:**
- **HIGH:** 10+ learning interactions showing consistent preference, same preference across 2+ projects
- **MEDIUM:** 5-9 learning interactions, OR consistent within 1 project only
- **LOW:** < 5 learning interactions, OR preference varies by topic familiarity
- **UNSCORED:** 0 messages with learning-relevant signals
**Example quotes:**
- **self-directed:** "I read through the middleware code. The issue is that the token check happens after the rate limiter. Should those be swapped?"
- **guided:** "Can you walk me through how the auth flow works in this codebase? Start from the login request."
- **documentation-first:** "I read the Prisma docs on relations. Can you help me apply the many-to-many pattern from their guide to our schema?"
- **example-driven:** "Show me a working example of a protected API route with JWT validation. I'll adapt it for our endpoints."
**Context-dependent patterns:**
Learning style often varies with domain expertise. A developer may be self-directed in familiar domains but guided or example-driven in new ones. Report the split if detected: "context-dependent: self-directed for TypeScript/Node, example-driven for Rust/systems programming."
---
## Evidence Curation
### Evidence Format
Use the combined format for each evidence entry:
**Signal:** [pattern interpretation -- what the quote demonstrates] / **Example:** "[trimmed quote, ~100 characters]" -- project: [project name]
### Evidence Targets
- **3 evidence quotes per dimension** (24 total across all 8 dimensions)
- Select quotes that best illustrate the rated pattern
- Prefer quotes from different projects to demonstrate cross-project consistency
- When fewer than 3 relevant quotes exist, include what is available and note the evidence count
### Quote Truncation
- Trim quotes to the behavioral signal -- the part that demonstrates the pattern
- Target approximately 100 characters per quote
- Preserve the meaningful fragment, not the full message
- If the signal is in the middle of a long message, use "..." to indicate trimming
- Never include the full 500-character message when 50 characters capture the signal
### Project Attribution
- Every evidence quote must include the project name
- Project attribution enables verification and shows cross-project patterns
- Format: `-- project: [name]`
### Sensitive Content Exclusion (Layer 1)
The profiler agent must never select quotes containing any of the following patterns:
- `sk-` (API key prefixes)
- `Bearer ` (auth tokens)
- `password` (credentials)
- `secret` (secrets)
- `token` (when used as a credential value, not a concept discussion)
- `api_key` or `API_KEY` (API key references)
- Full absolute file paths containing usernames (e.g., `/Users/john/...`, `/home/john/...`)
**When sensitive content is found and excluded**, report as metadata in the analysis output:
```json
{
"sensitive_excluded": [
{ "type": "api_key_pattern", "count": 2 },
{ "type": "file_path_with_username", "count": 1 }
]
}
```
This metadata enables defense-in-depth auditing. Layer 2 (regex filter in the write-profile step) provides a second pass, but the profiler should still avoid selecting sensitive quotes.
### Natural Language Priority
Weight natural language messages higher than:
- Pasted log output (detected by timestamps, repeated format strings, `[DEBUG]`, `[INFO]`, `[ERROR]`)
- Session context dumps (messages starting with "This session is being continued from a previous conversation")
- Large code pastes (messages where > 80% of content is inside code fences)
These message types are genuine but carry less behavioral signal. Deprioritize them when selecting evidence quotes.
---
## Recency Weighting
### Guideline
Recent sessions (last 30 days) should be weighted approximately 3x compared to older sessions when analyzing patterns.
### Rationale
Developer styles evolve. A developer who was terse six months ago may now provide detailed structured context. Recent behavior is a more accurate reflection of current working style.
### Application
1. When counting signals for confidence scoring, recent signals count 3x (e.g., 4 recent signals = 12 weighted signals)
2. When selecting evidence quotes, prefer recent quotes over older ones when both demonstrate the same pattern
3. When patterns conflict between recent and older sessions, the recent pattern takes precedence for the rating, but note the evolution: "recently shifted from terse-direct to conversational"
4. The 30-day window is relative to the analysis date, not a fixed date
### Edge Cases
- If ALL sessions are older than 30 days, apply no weighting (all sessions are equally stale)
- If ALL sessions are within the last 30 days, apply no weighting (all sessions are equally recent)
- The 3x weight is a guideline, not a hard multiplier -- use judgment when the weighted count changes a confidence threshold
---
## Thin Data Handling
### Message Thresholds
| Total Genuine Messages | Mode | Behavior |
|------------------------|------|----------|
| > 50 | `full` | Full analysis across all 8 dimensions. Questionnaire optional (user can choose to supplement). |
| 20-50 | `hybrid` | Analyze available messages. Score each dimension with confidence. Supplement with questionnaire for LOW/UNSCORED dimensions. |
| < 20 | `insufficient` | All dimensions scored LOW or UNSCORED. Recommend questionnaire fallback as primary profile source. Note: "insufficient session data for behavioral analysis." |
### Handling Insufficient Dimensions
When a specific dimension has insufficient data (even if total messages exceed thresholds):
- Set confidence to `UNSCORED`
- Set summary to: "Insufficient data -- no clear signals detected for this dimension."
- Set claude_instruction to a neutral fallback: "No strong preference detected. Ask the developer when this dimension is relevant."
- Set evidence_quotes to empty array `[]`
- Set evidence_count to `0`
### Questionnaire Supplement
When operating in `hybrid` mode, the questionnaire fills gaps for dimensions where session analysis produced LOW or UNSCORED confidence. The questionnaire-derived ratings use:
- **MEDIUM** confidence for strong, definitive picks
- **LOW** confidence for "it varies" or ambiguous selections
If session analysis and questionnaire agree on a dimension, confidence can be elevated (e.g., session LOW + questionnaire MEDIUM agreement = MEDIUM).
---
## Output Schema
The profiler agent must return JSON matching this exact schema, wrapped in `<analysis>` tags.
```json
{
"profile_version": "1.0",
"analyzed_at": "ISO-8601 timestamp",
"data_source": "session_analysis",
"projects_analyzed": ["project-name-1", "project-name-2"],
"messages_analyzed": 0,
"message_threshold": "full|hybrid|insufficient",
"sensitive_excluded": [
{ "type": "string", "count": 0 }
],
"dimensions": {
"communication_style": {
"rating": "terse-direct|conversational|detailed-structured|mixed",
"confidence": "HIGH|MEDIUM|LOW|UNSCORED",
"evidence_count": 0,
"cross_project_consistent": true,
"evidence_quotes": [
{
"signal": "Pattern interpretation describing what the quote demonstrates",
"quote": "Trimmed quote, approximately 100 characters",
"project": "project-name"
}
],
"summary": "One to two sentence description of the observed pattern",
"claude_instruction": "Imperative directive for Claude: 'Match structured communication style' not 'You tend to provide structured context'"
},
"decision_speed": {
"rating": "fast-intuitive|deliberate-informed|research-first|delegator",
"confidence": "HIGH|MEDIUM|LOW|UNSCORED",
"evidence_count": 0,
"cross_project_consistent": true,
"evidence_quotes": [],
"summary": "string",
"claude_instruction": "string"
},
"explanation_depth": {
"rating": "code-only|concise|detailed|educational",
"confidence": "HIGH|MEDIUM|LOW|UNSCORED",
"evidence_count": 0,
"cross_project_consistent": true,
"evidence_quotes": [],
"summary": "string",
"claude_instruction": "string"
},
"debugging_approach": {
"rating": "fix-first|diagnostic|hypothesis-driven|collaborative",
"confidence": "HIGH|MEDIUM|LOW|UNSCORED",
"evidence_count": 0,
"cross_project_consistent": true,
"evidence_quotes": [],
"summary": "string",
"claude_instruction": "string"
},
"ux_philosophy": {
"rating": "function-first|pragmatic|design-conscious|backend-focused",
"confidence": "HIGH|MEDIUM|LOW|UNSCORED",
"evidence_count": 0,
"cross_project_consistent": true,
"evidence_quotes": [],
"summary": "string",
"claude_instruction": "string"
},
"vendor_philosophy": {
"rating": "pragmatic-fast|conservative|thorough-evaluator|opinionated",
"confidence": "HIGH|MEDIUM|LOW|UNSCORED",
"evidence_count": 0,
"cross_project_consistent": true,
"evidence_quotes": [],
"summary": "string",
"claude_instruction": "string"
},
"frustration_triggers": {
"rating": "scope-creep|instruction-adherence|verbosity|regression",
"confidence": "HIGH|MEDIUM|LOW|UNSCORED",
"evidence_count": 0,
"cross_project_consistent": true,
"evidence_quotes": [],
"summary": "string",
"claude_instruction": "string"
},
"learning_style": {
"rating": "self-directed|guided|documentation-first|example-driven",
"confidence": "HIGH|MEDIUM|LOW|UNSCORED",
"evidence_count": 0,
"cross_project_consistent": true,
"evidence_quotes": [],
"summary": "string",
"claude_instruction": "string"
}
}
}
```
### Schema Notes
- **`profile_version`**: Always `"1.0"` for this schema version
- **`analyzed_at`**: ISO-8601 timestamp of when the analysis was performed
- **`data_source`**: `"session_analysis"` for session-based profiling, `"questionnaire"` for questionnaire-only, `"hybrid"` for combined
- **`projects_analyzed`**: List of project names that contributed messages
- **`messages_analyzed`**: Total number of genuine user messages processed
- **`message_threshold`**: Which threshold mode was triggered (`full`, `hybrid`, `insufficient`)
- **`sensitive_excluded`**: Array of excluded sensitive content types with counts (empty array if none found)
- **`claude_instruction`**: Must be written in imperative form directed at Claude. This field is how the profile becomes actionable.
- Good: "Provide structured responses with headers and numbered lists to match this developer's communication style."
- Bad: "You tend to like structured responses."
- Good: "Ask before making changes beyond the stated request -- this developer values bounded execution."
- Bad: "The developer gets frustrated when you do extra work."
---
## Cross-Project Consistency
### Assessment
For each dimension, assess whether the observed pattern is consistent across the projects analyzed:
- **`cross_project_consistent: true`** -- Same rating would apply regardless of which project is analyzed. Evidence from 2+ projects shows the same pattern.
- **`cross_project_consistent: false`** -- Pattern varies by project. Include a context-dependent note in the summary.
### Reporting Splits
When `cross_project_consistent` is false, the summary must describe the split:
- "Context-dependent: terse-direct for CLI/backend projects (gsd-tools, api-server), detailed-structured for frontend projects (dashboard, landing-page)."
- "Context-dependent: fast-intuitive for familiar tech (React, Node), research-first for new domains (Rust, ML)."
The rating field should reflect the **dominant** pattern (most evidence). The summary describes the nuance.
### Phase 3 Resolution
Context-dependent splits are resolved during Phase 3 orchestration. The orchestrator presents the split to the developer and asks which pattern represents their general preference. Until resolved, Claude uses the dominant pattern with awareness of the context-dependent variation.
---
*Reference document version: 1.0*
*Dimensions: 8*
*Schema: profile_version 1.0*

View File

@@ -0,0 +1,105 @@
# CLAUDE.md Template
Template for project-root `CLAUDE.md` — auto-generated by `gsd-tools generate-claude-md`.
Contains 5 marker-bounded sections. Each section is independently updatable.
The `generate-claude-md` subcommand manages 4 sections (project, stack, conventions, architecture).
The profile section is managed exclusively by `generate-claude-profile`.
---
## Section Templates
### Project Section
```
<!-- GSD:project-start source:PROJECT.md -->
## Project
{{project_content}}
<!-- GSD:project-end -->
```
**Fallback text:**
```
Project not yet initialized. Run /gsd:new-project to set up.
```
### Stack Section
```
<!-- GSD:stack-start source:STACK.md -->
## Technology Stack
{{stack_content}}
<!-- GSD:stack-end -->
```
**Fallback text:**
```
Technology stack not yet documented. Will populate after codebase mapping or first phase.
```
### Conventions Section
```
<!-- GSD:conventions-start source:CONVENTIONS.md -->
## Conventions
{{conventions_content}}
<!-- GSD:conventions-end -->
```
**Fallback text:**
```
Conventions not yet established. Will populate as patterns emerge during development.
```
### Architecture Section
```
<!-- GSD:architecture-start source:ARCHITECTURE.md -->
## Architecture
{{architecture_content}}
<!-- GSD:architecture-end -->
```
**Fallback text:**
```
Architecture not yet mapped. Follow existing patterns found in the codebase.
```
### Profile Section (Placeholder Only)
```
<!-- GSD:profile-start -->
## Developer Profile
> Profile not yet configured. Run `/gsd:profile-user` to generate your developer profile.
> This section is managed by `generate-claude-profile` — do not edit manually.
<!-- GSD:profile-end -->
```
**Note:** This section is NOT managed by `generate-claude-md`. It is managed exclusively
by `generate-claude-profile`. The placeholder above is only used when creating a new
CLAUDE.md file and no profile section exists yet.
---
## Section Ordering
1. **Project** — Identity and purpose (what this project is)
2. **Stack** — Technology choices (what tools are used)
3. **Conventions** — Code patterns and rules (how code is written)
4. **Architecture** — System structure (how components fit together)
5. **Profile** — Developer behavioral preferences (how to interact)
## Marker Format
- Start: `<!-- GSD:{name}-start source:{file} -->`
- End: `<!-- GSD:{name}-end -->`
- Source attribute enables targeted updates when source files change
- Partial match on start marker (without closing `-->`) for detection
## Fallback Behavior
When a source file is missing, fallback text provides Claude-actionable guidance:
- Guides Claude's behavior in the absence of data
- Not placeholder ads or "missing" notices
- Each fallback tells Claude what to do, not just what's absent

View File

@@ -0,0 +1,21 @@
---
description: Load developer preferences into this session
---
# Developer Preferences
> Generated by GSD on {{generated_at}} from {{data_source}}.
> Run `/gsd:profile-user --refresh` to regenerate.
## Behavioral Directives
Follow these directives when working with this developer. Higher confidence
directives should be applied directly. Lower confidence directives should be
tried with hedging ("Based on your profile, I'll try X -- let me know if
that's off").
{{behavioral_directives}}
## Stack Preferences
{{stack_preferences}}

View File

@@ -0,0 +1,146 @@
# Developer Profile
> This profile was generated from session analysis. It contains behavioral directives
> for Claude to follow when working with this developer. HIGH confidence dimensions
> should be acted on directly. LOW confidence dimensions should be approached with
> hedging ("Based on your profile, I'll try X -- let me know if that's off").
**Generated:** {{generated_at}}
**Source:** {{data_source}}
**Projects Analyzed:** {{projects_list}}
**Messages Analyzed:** {{message_count}}
---
## Quick Reference
{{summary_instructions}}
---
## Communication Style
**Rating:** {{communication_style.rating}} | **Confidence:** {{communication_style.confidence}}
**Directive:** {{communication_style.claude_instruction}}
{{communication_style.summary}}
**Evidence:**
{{communication_style.evidence}}
---
## Decision Speed
**Rating:** {{decision_speed.rating}} | **Confidence:** {{decision_speed.confidence}}
**Directive:** {{decision_speed.claude_instruction}}
{{decision_speed.summary}}
**Evidence:**
{{decision_speed.evidence}}
---
## Explanation Depth
**Rating:** {{explanation_depth.rating}} | **Confidence:** {{explanation_depth.confidence}}
**Directive:** {{explanation_depth.claude_instruction}}
{{explanation_depth.summary}}
**Evidence:**
{{explanation_depth.evidence}}
---
## Debugging Approach
**Rating:** {{debugging_approach.rating}} | **Confidence:** {{debugging_approach.confidence}}
**Directive:** {{debugging_approach.claude_instruction}}
{{debugging_approach.summary}}
**Evidence:**
{{debugging_approach.evidence}}
---
## UX Philosophy
**Rating:** {{ux_philosophy.rating}} | **Confidence:** {{ux_philosophy.confidence}}
**Directive:** {{ux_philosophy.claude_instruction}}
{{ux_philosophy.summary}}
**Evidence:**
{{ux_philosophy.evidence}}
---
## Vendor Philosophy
**Rating:** {{vendor_philosophy.rating}} | **Confidence:** {{vendor_philosophy.confidence}}
**Directive:** {{vendor_philosophy.claude_instruction}}
{{vendor_philosophy.summary}}
**Evidence:**
{{vendor_philosophy.evidence}}
---
## Frustration Triggers
**Rating:** {{frustration_triggers.rating}} | **Confidence:** {{frustration_triggers.confidence}}
**Directive:** {{frustration_triggers.claude_instruction}}
{{frustration_triggers.summary}}
**Evidence:**
{{frustration_triggers.evidence}}
---
## Learning Style
**Rating:** {{learning_style.rating}} | **Confidence:** {{learning_style.confidence}}
**Directive:** {{learning_style.claude_instruction}}
{{learning_style.summary}}
**Evidence:**
{{learning_style.evidence}}
---
## Profile Metadata
| Field | Value |
|-------|-------|
| Profile Version | {{profile_version}} |
| Generated | {{generated_at}} |
| Source | {{data_source}} |
| Projects | {{projects_count}} |
| Messages | {{message_count}} |
| Dimensions Scored | {{dimensions_scored}}/8 |
| High Confidence | {{high_confidence_count}} |
| Medium Confidence | {{medium_confidence_count}} |
| Low Confidence | {{low_confidence_count}} |
| Sensitive Content Excluded | {{sensitive_excluded_summary}} |

View File

@@ -0,0 +1,450 @@
<purpose>
Orchestrate the full developer profiling flow: consent, session analysis (or questionnaire fallback), profile generation, result display, and artifact creation.
This workflow wires Phase 1 (session pipeline) and Phase 2 (profiling engine) into a cohesive user-facing experience. All heavy lifting is done by existing gsd-tools.cjs subcommands and the gsd-user-profiler agent -- this workflow orchestrates the sequence, handles branching, and provides the UX.
</purpose>
<required_reading>
Read all files referenced by the invoking prompt's execution_context before starting.
Key references:
- @$HOME/.claude/get-shit-done/references/ui-brand.md (display patterns)
- @$HOME/.claude/get-shit-done/agents/gsd-user-profiler.md (profiler agent definition)
- @$HOME/.claude/get-shit-done/references/user-profiling.md (profiling reference doc)
</required_reading>
<process>
## 1. Initialize
Parse flags from $ARGUMENTS:
- Detect `--questionnaire` flag (skip session analysis, questionnaire-only)
- Detect `--refresh` flag (rebuild profile even when one exists)
Check for existing profile:
```bash
PROFILE_PATH="$HOME/.claude/get-shit-done/USER-PROFILE.md"
[ -f "$PROFILE_PATH" ] && echo "EXISTS" || echo "NOT_FOUND"
```
**If profile exists AND --refresh NOT set AND --questionnaire NOT set:**
Use AskUserQuestion:
- header: "Existing Profile"
- question: "You already have a profile. What would you like to do?"
- options:
- "View it" -- Display summary card from existing profile data, then exit
- "Refresh it" -- Continue with --refresh behavior
- "Cancel" -- Exit workflow
If "View it": Read USER-PROFILE.md, display its content formatted as a summary card, then exit.
If "Refresh it": Set --refresh behavior and continue.
If "Cancel": Display "No changes made." and exit.
**If profile exists AND --refresh IS set:**
Backup existing profile:
```bash
cp "$HOME/.claude/get-shit-done/USER-PROFILE.md" "$HOME/.claude/get-shit-done/USER-PROFILE.backup.md"
```
Display: "Re-analyzing your sessions to update your profile."
Continue to step 2.
**If no profile exists:** Continue to step 2.
---
## 2. Consent Gate (ACTV-06)
**Skip if** `--questionnaire` flag is set (no JSONL reading occurs -- jump directly to step 4b).
Display consent screen:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD > PROFILE YOUR CODING STYLE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Claude starts every conversation generic. A profile teaches Claude
how YOU actually work -- not how you think you work.
## What We'll Analyze
Your recent Claude Code sessions, looking for patterns in these
8 behavioral dimensions:
| Dimension | What It Measures |
|----------------------|---------------------------------------------|
| Communication Style | How you phrase requests (terse vs. detailed) |
| Decision Speed | How you choose between options |
| Explanation Depth | How much explanation you want with code |
| Debugging Approach | How you tackle errors and bugs |
| UX Philosophy | How much you care about design vs. function |
| Vendor Philosophy | How you evaluate libraries and tools |
| Frustration Triggers | What makes you correct Claude |
| Learning Style | How you prefer to learn new things |
## Data Handling
✓ Reads session files locally (read-only, nothing modified)
✓ Analyzes message patterns (not content meaning)
✓ Stores profile at $HOME/.claude/get-shit-done/USER-PROFILE.md
✗ Nothing is sent to external services
✗ Sensitive content (API keys, passwords) is automatically excluded
```
**If --refresh path:**
Show abbreviated consent instead:
```
Re-analyzing your sessions to update your profile.
Your existing profile has been backed up to USER-PROFILE.backup.md.
```
Use AskUserQuestion:
- header: "Refresh"
- question: "Continue with profile refresh?"
- options:
- "Continue" -- Proceed to step 3
- "Cancel" -- Exit workflow
**If default (no --refresh) path:**
Use AskUserQuestion:
- header: "Ready?"
- question: "Ready to analyze your sessions?"
- options:
- "Let's go" -- Proceed to step 3 (session analysis)
- "Use questionnaire instead" -- Jump to step 4b (questionnaire path)
- "Not now" -- Display "No worries. Run /gsd:profile-user when ready." and exit
---
## 3. Session Scan
Display: "◆ Scanning sessions..."
Run session scan:
```bash
SCAN_RESULT=$(node $HOME/.claude/get-shit-done/bin/gsd-tools.cjs scan-sessions --json 2>/dev/null)
```
Parse the JSON output to get session count and project count.
Display: "✓ Found N sessions across M projects"
**Determine data sufficiency:**
- Count total messages available from the scan result (sum sessions across projects)
- If 0 sessions found: Display "No sessions found. Switching to questionnaire." and jump to step 4b
- If sessions found: Continue to step 4a
---
## 4a. Session Analysis Path
Display: "◆ Sampling messages..."
Run profile sampling:
```bash
SAMPLE_RESULT=$(node $HOME/.claude/get-shit-done/bin/gsd-tools.cjs profile-sample --json 2>/dev/null)
```
Parse the JSON output to get the temp directory path and message count.
Display: "✓ Sampled N messages from M projects"
Display: "◆ Analyzing patterns..."
**Spawn gsd-user-profiler agent using Task tool:**
Use the Task tool to spawn the `gsd-user-profiler` agent. Provide it with:
- The sampled JSONL file path from profile-sample output
- The user-profiling reference doc at `$HOME/.claude/get-shit-done/references/user-profiling.md`
The agent prompt should follow this structure:
```
Read the profiling reference document and the sampled session messages, then analyze the developer's behavioral patterns across all 8 dimensions.
Reference: @$HOME/.claude/get-shit-done/references/user-profiling.md
Session data: @{temp_dir}/profile-sample.jsonl
Analyze these messages and return your analysis in the <analysis> JSON format specified in the reference document.
```
**Parse the agent's output:**
- Extract the `<analysis>` JSON block from the agent's response
- Save analysis JSON to a temp file (in the same temp directory created by profile-sample)
```bash
ANALYSIS_PATH="{temp_dir}/analysis.json"
```
Write the analysis JSON to `$ANALYSIS_PATH`.
Display: "✓ Analysis complete (N dimensions scored)"
**Check for thin data:**
- Read the analysis JSON and check the total message count
- If < 50 messages were analyzed: Note that a questionnaire supplement could improve accuracy. Display: "Note: Limited session data (N messages). Results may have lower confidence."
Continue to step 5.
---
## 4b. Questionnaire Path
Display: "Using questionnaire to build your profile."
**Get questions:**
```bash
QUESTIONS=$(node $HOME/.claude/get-shit-done/bin/gsd-tools.cjs profile-questionnaire --json 2>/dev/null)
```
Parse the questions JSON. It contains 8 questions, one per dimension.
**Present each question to the user via AskUserQuestion:**
For each question in the questions array:
- header: The dimension name (e.g., "Communication Style")
- question: The question text
- options: The answer options from the question definition
Collect all answers into an answers JSON object mapping dimension keys to selected answer values.
**Save answers to temp file:**
```bash
ANSWERS_PATH=$(mktemp /tmp/gsd-profile-answers-XXXXXX.json)
```
Write the answers JSON to `$ANSWERS_PATH`.
**Convert answers to analysis:**
```bash
ANALYSIS_RESULT=$(node $HOME/.claude/get-shit-done/bin/gsd-tools.cjs profile-questionnaire --answers "$ANSWERS_PATH" --json 2>/dev/null)
```
Parse the analysis JSON from the result.
Save analysis JSON to a temp file:
```bash
ANALYSIS_PATH=$(mktemp /tmp/gsd-profile-analysis-XXXXXX.json)
```
Write the analysis JSON to `$ANALYSIS_PATH`.
Continue to step 5 (skip split resolution since questionnaire handles ambiguity internally).
---
## 5. Split Resolution
**Skip if** questionnaire-only path (splits already handled internally).
Read the analysis JSON from `$ANALYSIS_PATH`.
Check each dimension for `cross_project_consistent: false`.
**For each split detected:**
Use AskUserQuestion:
- header: The dimension name (e.g., "Communication Style")
- question: "Your sessions show different patterns:" followed by the split context (e.g., "CLI/backend projects -> terse-direct, Frontend/UI projects -> detailed-structured")
- options:
- Rating option A (e.g., "terse-direct")
- Rating option B (e.g., "detailed-structured")
- "Context-dependent (keep both)"
**If user picks a specific rating:** Update the dimension's `rating` field in the analysis JSON to the selected value.
**If user picks "Context-dependent":** Keep the dominant rating in the `rating` field. Add a `context_note` to the dimension's summary describing the split (e.g., "Context-dependent: terse in CLI projects, detailed in frontend projects").
Write updated analysis JSON back to `$ANALYSIS_PATH`.
---
## 6. Profile Write
Display: "◆ Writing profile..."
```bash
node $HOME/.claude/get-shit-done/bin/gsd-tools.cjs write-profile --input "$ANALYSIS_PATH" --json 2>/dev/null
```
Display: "✓ Profile written to $HOME/.claude/get-shit-done/USER-PROFILE.md"
---
## 7. Result Display
Read the analysis JSON from `$ANALYSIS_PATH` to build the display.
**Show report card table:**
```
## Your Profile
| Dimension | Rating | Confidence |
|----------------------|----------------------|------------|
| Communication Style | detailed-structured | HIGH |
| Decision Speed | deliberate-informed | MEDIUM |
| Explanation Depth | concise | HIGH |
| Debugging Approach | hypothesis-driven | MEDIUM |
| UX Philosophy | pragmatic | LOW |
| Vendor Philosophy | thorough-evaluator | HIGH |
| Frustration Triggers | scope-creep | MEDIUM |
| Learning Style | self-directed | HIGH |
```
(Populate with actual values from the analysis JSON.)
**Show highlight reel:**
Pick 3-4 dimensions with the highest confidence and most evidence signals. Format as:
```
## Highlights
- **Communication (HIGH):** You consistently provide structured context with
headers and problem statements before making requests
- **Vendor Choices (HIGH):** You research alternatives thoroughly -- comparing
docs, GitHub activity, and bundle sizes before committing
- **Frustrations (MEDIUM):** You correct Claude most often for doing things
you didn't ask for -- scope creep is your primary trigger
```
Build highlights from the `evidence` array and `summary` fields in the analysis JSON. Use the most compelling evidence quotes. Format each as "You tend to..." or "You consistently..." with evidence attribution.
**Offer full profile view:**
Use AskUserQuestion:
- header: "Profile"
- question: "Want to see the full profile?"
- options:
- "Yes" -- Read and display the full USER-PROFILE.md content, then continue to step 8
- "Continue to artifacts" -- Proceed directly to step 8
---
## 8. Artifact Selection (ACTV-05)
Use AskUserQuestion with multiSelect:
- header: "Artifacts"
- question: "Which artifacts should I generate?"
- options (ALL pre-selected by default):
- "/gsd:dev-preferences command file" -- "Load your preferences in any session"
- "CLAUDE.md profile section" -- "Add profile to this project's CLAUDE.md"
- "Global CLAUDE.md" -- "Add profile to $HOME/.claude/CLAUDE.md for all projects"
**If no artifacts selected:** Display "No artifacts generated. Your profile is saved at $HOME/.claude/get-shit-done/USER-PROFILE.md" and jump to step 10.
---
## 9. Artifact Generation
Generate selected artifacts sequentially (file I/O is fast, no benefit from parallel agents):
**For /gsd:dev-preferences (if selected):**
```bash
node $HOME/.claude/get-shit-done/bin/gsd-tools.cjs generate-dev-preferences --analysis "$ANALYSIS_PATH" --json 2>/dev/null
```
Display: "✓ Generated /gsd:dev-preferences at $HOME/.claude/commands/gsd/dev-preferences.md"
**For CLAUDE.md profile section (if selected):**
```bash
node $HOME/.claude/get-shit-done/bin/gsd-tools.cjs generate-claude-profile --analysis "$ANALYSIS_PATH" --json 2>/dev/null
```
Display: "✓ Added profile section to CLAUDE.md"
**For Global CLAUDE.md (if selected):**
```bash
node $HOME/.claude/get-shit-done/bin/gsd-tools.cjs generate-claude-profile --analysis "$ANALYSIS_PATH" --global --json 2>/dev/null
```
Display: "✓ Added profile section to $HOME/.claude/CLAUDE.md"
**Error handling:** If any gsd-tools.cjs call fails, display the error message and use AskUserQuestion to offer "Retry" or "Skip this artifact". On retry, re-run the command. On skip, continue to next artifact.
---
## 10. Summary & Refresh Diff
**If --refresh path:**
Read both old backup and new analysis to compare dimension ratings/confidence.
Read the backed-up profile:
```bash
BACKUP_PATH="$HOME/.claude/get-shit-done/USER-PROFILE.backup.md"
```
Compare each dimension's rating and confidence between old and new. Display diff table showing only changed dimensions:
```
## Changes
| Dimension | Before | After |
|-----------------|-----------------------------|-----------------------------|
| Communication | terse-direct (LOW) | detailed-structured (HIGH) |
| Debugging | fix-first (MEDIUM) | hypothesis-driven (MEDIUM) |
```
If nothing changed: Display "No changes detected -- your profile is already up to date."
**Display final summary:**
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD > PROFILE COMPLETE ✓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Your profile: $HOME/.claude/get-shit-done/USER-PROFILE.md
```
Then list paths for each generated artifact:
```
Artifacts:
✓ /gsd:dev-preferences $HOME/.claude/commands/gsd/dev-preferences.md
✓ CLAUDE.md section ./CLAUDE.md
✓ Global CLAUDE.md $HOME/.claude/CLAUDE.md
```
(Only show artifacts that were actually generated.)
**Clean up temp files:**
Remove the temp directory created by profile-sample (contains sample JSONL and analysis JSON):
```bash
rm -rf "$TEMP_DIR"
```
Also remove any standalone temp files created for questionnaire answers:
```bash
rm -f "$ANSWERS_PATH" 2>/dev/null
rm -f "$ANALYSIS_PATH" 2>/dev/null
```
(Only clean up temp paths that were actually created during this workflow run.)
</process>
<success_criteria>
- [ ] Initialization detects existing profile and handles all three responses (view/refresh/cancel)
- [ ] Consent gate shown for session analysis path, skipped for questionnaire path
- [ ] Session scan discovers sessions and reports statistics
- [ ] Session analysis path: samples messages, spawns profiler agent, extracts analysis JSON
- [ ] Questionnaire path: presents 8 questions, collects answers, converts to analysis JSON
- [ ] Split resolution presents context-dependent splits with user resolution options
- [ ] Profile written to USER-PROFILE.md via write-profile subcommand
- [ ] Result display shows report card table and highlight reel with evidence
- [ ] Artifact selection uses multiSelect with all options pre-selected
- [ ] Artifacts generated sequentially via gsd-tools.cjs subcommands
- [ ] Refresh diff shows changed dimensions when --refresh was used
- [ ] Temp files cleaned up on completion
</success_criteria>

View File

@@ -746,10 +746,10 @@ describe('Copilot agent conversion - real files', () => {
assert.ok(toolsLine.includes("'read'"), 'Read mapped');
});
test('all 15 agents convert without error', () => {
test('all 16 agents convert without error', () => {
const agents = fs.readdirSync(agentsSrc)
.filter(f => f.startsWith('gsd-') && f.endsWith('.md'));
assert.strictEqual(agents.length, 15, `expected 15 agents, got ${agents.length}`);
assert.strictEqual(agents.length, 16, `expected 16 agents, got ${agents.length}`);
for (const agentFile of agents) {
const content = fs.readFileSync(path.join(agentsSrc, agentFile), 'utf8');
@@ -1202,6 +1202,7 @@ describe('E2E: Copilot full install verification', () => {
'gsd-ui-auditor.agent.md',
'gsd-ui-checker.agent.md',
'gsd-ui-researcher.agent.md',
'gsd-user-profiler.agent.md',
'gsd-verifier.agent.md',
].sort();
assert.deepStrictEqual(gsdAgents, expected);