Files
msd-core/get-shit-done/bin/lib/profile-output.cjs
Tom Boucher 2d32ad82be fix(plan-phase): remove agent: directive that caused OpenCode subagent dispatch (#3156) (#3206)
* feat(roadmap): parse **Mode:** field on phase sections

Adds a 'mode' field to roadmap.get-phase and roadmap.analyze outputs.
Recognizes '**Mode:** mvp' lines in phase sections; lowercased + trimmed.
Forward-compat: unrecognized values preserved verbatim, no enum check.

Foundation for --mvp flag in plan-phase (PRD: vertical-mvp-slice).

* feat(plan-phase): parse --mvp flag and resolve MVP_MODE

Resolution order: CLI flag → ROADMAP **Mode:** field → workflow.mvp_mode
config → false. Walking Skeleton gate fires for new-project Phase 1.
Wires MVP_MODE + WALKING_SKELETON into gsd-planner subagent prompt.

Per PRD vertical-mvp-slice Phase 1 (Q1, Q2, Q4).

* docs(planner): add vertical-slice planning reference

New reference loaded by gsd-planner when MVP_MODE=true. Defines slice
ordering, Walking Skeleton rules, and anti-patterns. Referenced from
plan-phase workflow MVP_MODE wiring.

* docs(planner): add SKELETON.md template

Template emitted by gsd-planner under WALKING_SKELETON=true. Captures
architectural decisions and out-of-scope list for new-project Phase 1.

* chore(inventory): register new planner references

Added planner-mvp-mode.md and skeleton-template.md to INVENTORY.md and
INVENTORY-MANIFEST.json. References now: 53.

* feat(gsd-planner): add MVP Mode Detection section

Mode-switched branch in the existing planner agent (per Q4: single agent).
Vertical-slice decomposition rules, Walking Skeleton handling, and
TDD-mode compatibility. Heavy guidance lives in references/planner-mvp-mode.md.

* test(plan-phase): add --mvp resolution-chain integration cases

Validates roadmap.get-phase --pick mode and confirms workflow.mvp_mode
default is unset in fresh projects.

* docs(changelog): announce --mvp vertical-slice planning (#2826)

* feat(mvp-phase): add /gsd mvp-phase slash command

Standalone command for vertical MVP planning. Frontmatter only;
heavyweight workflow at get-shit-done/workflows/mvp-phase.md follows
in next commit. Mirrors discuss-phase/edit-phase command shape.

* docs(planner): add user-story-template reference

Defines the canonical 'As a / I want to / So that' format and the
ROADMAP.md / PLAN.md emit rules. Used by mvp-phase workflow and
gsd-planner agent under MVP_MODE.

* docs(planner): add SPIDR splitting reference

Defines size signals, the five SPIDR axes (Spike/Paths/Interfaces/Data/Rules),
the interactive workflow, and anti-patterns. Per PRD Q3 decision: full
interactive flow, not lightweight check. Used by mvp-phase workflow.

* fix(mvp-phase): trim description to fit 100-char budget

* feat(mvp-phase): add mvp-phase workflow

Standalone workflow: phase validation -> user story prompts (As a / I want to /
So that) -> SPIDR splitting check -> ROADMAP write (Mode + Goal) -> delegation
to plan-phase. Per PRD Phase 2 (Q3 full SPIDR; Phase-2-A/B/C/D decisions).

Plan-phase auto-detects MVP via Phase 1's resolution chain, so no flags
are needed when delegating.

* feat(gsd-planner): emit user-story header in PLAN.md under MVP mode

Extends the MVP Mode Detection section (added in Phase 1) so the planner
sources the user story from ROADMAP **Goal:** and emits the bolded
**As a** / **I want to** / **so that** form as the first content under
the phase header in PLAN.md. References user-story-template.md.

* test(mvp-phase): integration smoke test for ROADMAP mutation

Validates roadmap.get-phase output after a workflow-spec'd ROADMAP write:
mode=mvp and goal=full user story. Catches schema drift between workflow
emit and parser expectation. Includes a long-story case (>120 chars) to
confirm SPIDR-rejected stories still parse correctly.

* chore(inventory): register mvp-phase command + 2 new references

Adds /gsd mvp-phase to commands list, mvp-phase workflow to workflows list,
and user-story-template.md + spidr-splitting.md to references. References
count: 53 -> 55.

* docs(changelog): announce /gsd mvp-phase command (#2826)

* fix(mvp-phase): add TEXT_MODE plain-text fallback for non-Claude runtimes (#2012)

* docs(executor): add MVP+TDD gate reference

Defines the runtime gate semantics for execute-phase when both
MVP_MODE and TDD_MODE are true: pre-task verification of failing-test
commit, end-of-phase review escalation from advisory to blocking,
behavior-adding task definition. Loaded conditionally by
execute-phase workflow and gsd-executor agent.

* feat(execute-phase): MVP+TDD runtime gate + blocking review

Resolves MVP_MODE in Step 1 (CLI flag -> roadmap mode -> config -> false).
Adds per-task gate that halts before behavior-adding tasks run if no
failing-test commit exists for the plan. Escalates end-of-phase TDD
review from advisory to blocking when both MVP_MODE and TDD_MODE active.

Also updates INVENTORY-MANIFEST.json to register execute-mvp-tdd.md
(added by Task 1) so manifest-sync tests pass.

Per PRD vertical-mvp-slice Phase 3a (decisions Phase-3-A, Phase-3-Split).

* feat(gsd-executor): add MVP+TDD Gate section

Mirrors the planner's MVP Mode Detection pattern from Phase 1.
Instructs halt-and-report when the runtime gate trips, references
execute-mvp-tdd.md for full semantics. No agent changes outside the
new section.

* test(execute-phase): add MVP+TDD resolution-chain integration cases

Validates roadmap.get-phase --pick mode and confirms workflow.mvp_mode
default is unset in fresh projects. Mirrors the Phase 1 plan-phase
resolution-chain integration test.

* chore(inventory): register execute-mvp-tdd reference

Bumps References count 55 -> 56. Registers execute-mvp-tdd.md.
Adds "init" to PROSE_ALLOWLIST in registry integration test so
bare `gsd-sdk query init` prose examples in plan docs don't
trigger the unregistered-handler guard (real commands are all
init.<subcommand>).

* docs(changelog): announce MVP+TDD runtime gate in execute-phase (#2826)

* docs(verifier): add verify-mvp-mode reference

Defines UAT framing under MVP mode: user-flow walk-through first,
technical checks deferred, coverage check as goal-backward narrowing
to the user story's outcome clause. Loaded conditionally by
verify-work workflow and gsd-verifier agent.

* feat(verify-work): MVP-mode UAT framing — user flow first

Resolves MVP_MODE from phase mode field. Under MVP mode, generates UAT
in three ordered sections: user-flow walk-through (derived from user
story), technical checks (deferred), coverage check (goal-backward).
Falls back to standard UAT generation when mode is null/absent.
User-story-format guard refuses to verify a mode:mvp phase with a
non-user-story goal.

Also updates docs/INVENTORY.md (56 references) and
docs/INVENTORY-MANIFEST.json to register verify-mvp-mode.md added
in Task 1.

Per PRD vertical-mvp-slice Phase 3b (decisions Phase-3-B,
Phase-3-Verify-Structure).

* feat(gsd-verifier): add MVP Mode Verification section

Narrows goal-backward verification to the user-story [outcome] clause
when phase mode is mvp. References verify-mvp-mode.md. Preserves
existing goal-backward methodology for non-MVP phases. User-story-format
guard refuses to verify a mode:mvp phase with a non-user-story goal.

* docs(changelog): announce MVP-mode UAT framing in verify-work (#2826)

* feat(new-project): add Vertical MVP vs Horizontal Layers mode prompt

Asks user at project init how to structure the project. Vertical MVP
emits **Mode:** mvp on every initial roadmap phase (per-phase mode
preserved per PRD Q1). Horizontal Layers falls back to standard
template — no behavioral change for existing flows.

Per PRD vertical-mvp-slice Phase 4 (decision Phase-4-Persistence).

* feat(progress): add MVP-mode user-flow display

When phase has **Mode:** mvp, progress renders user-flow status from
PLAN.md task names alongside standard task progress. Tasks that aren't
user-flow-shaped (technical-sounding) are filtered out of the user-flow
sub-block. Falls back to standard display when mode is null/absent.

Per PRD vertical-mvp-slice Phase 4 (decision Phase-4-Progress).

* feat(stats): add MVP phase count summary

Reads roadmap.analyze (which surfaces mode per phase from Phase 1) and
emits 'Phases: N total | M MVP | K standard' summary line. Suppressed
when MVP_COUNT == 0 to avoid clutter on non-MVP projects.

Per PRD vertical-mvp-slice Phase 4.

* feat(graphify): add MVP-mode visual differentiation

MVP-mode phases render with #22c55e fill color AND ' (MVP)' label
suffix — two-channel signaling for color-blind and grayscale renders.
Standard phases unchanged.

Per PRD vertical-mvp-slice Phase 4 (PRD Q5: distinct visual treatment).

* docs(changelog): announce Phase 4 discovery & progress (#2826)

* chore(release): bump dev to 1.50.0-canary.0 for first 1.50.0 canary

Sets the base version that .github/workflows/canary.yml derives the canary
tag from (strips suffix → base 1.50.0 → next available v1.50.0-canary.N).

This kicks off the 1.50.0 release train, opened by the MVP/TDD/UAT vertical
slice landed across PRs #2867, #2874, #2878, #2880, #2883.

* docs: add CANARY stream README + v1.50.0-canary.1 release notes

- docs/CANARY.md — explains the dev→@canary stream policy, install/rollback
  paths, and when (not) to install canary builds
- docs/RELEASE-v1.50.0-canary.1.md — release notes for the first 1.50.0
  canary cut: vertical MVP/TDD/UAT slice (#2867 + #2874 + #2878 + #2880 +
  #2883), opening the 1.50.0 train under PRD #2826
- docs/README.md — index entry + quick link for the canary stream

* fix(ci/canary): publish gate checks dev branch, not main

Four publish-step `if:` conditions in .github/workflows/canary.yml were
checking `github.ref == 'refs/heads/main'`. Those steps (Tag and push,
Publish to npm, Publish SDK to npm, Verify publish) therefore always
skipped on every workflow_dispatch invocation since canary runs from dev,
never main.

The workflow's own header comment is unambiguous: `dev → @canary`. The
gate was a copy-paste from release.yml (which correctly targets main for
the @next/@latest streams) that was never corrected for the canary stream.

This is why the 1.50.0-canary.1 publish hadn't materialized despite three
green workflow runs. With the gate corrected, the next dispatch will
actually publish.

* ci(release-sdk): make release-sdk.yml dispatchable from the dev branch

The workflow lives on main only, so the GitHub Actions "Use workflow
from" dropdown doesn't list dev — meaning dev → @dev publishes can't be
triggered from the dev branch directly. Add the file to dev so an
operator can dispatch it with branch=dev and tag=dev.

Per project release-stream policy: dev branch publishes canary (@dev).
This is the stream that needs the file most, since main never publishes
@dev itself (main does @next / @latest).

File is byte-identical to main's release-sdk.yml — straight propagation,
no behavioral change. Tracking issues #2925, #2929.

* docs(mvp): canary-prep concept cleanup — CONTEXT.md, mvp-concepts index, --prd interaction (#3176)

* chore(mvp): concept cleanup + cross-ref index for v1.50.0-canary.2 prep

- CONTEXT.md gains 7 MVP domain terms (MVP Mode, User Story, Walking
  Skeleton, Vertical Slice, Behavior-Adding Task, MVP+TDD Gate, SPIDR
  Splitting) so the project glossary matches the shipped surface.
- New get-shit-done/references/mvp-concepts.md indexes the six MVP
  reference files and concept-to-file map so agents and contributors
  can find the right canonical doc without grepping.
- plan-phase.md Walking Skeleton block now documents that --mvp and
  --prd compose orthogonally on Phase 1; no precedence needed.
- INVENTORY/INVENTORY-MANIFEST refreshed for the new reference (58 -> 59).

No behavior change. Canary-prep cleanup ahead of v1.50.0-canary.2.

Surfaced for follow-up (not in this PR):
- MVP_MODE resolution shell block duplicated across plan-phase,
  execute-phase, verify-work workflows (needs a shared workflow-include
  mechanism; structural change).
- Behavior-Adding Task predicate is prose-only; no shared utility.
- User Story regex hardcoded in verify-work; would benefit from a
  central definition consumed by the verifier and the mvp-phase command.

* chore(changeset): set PR number for mvp concept cleanup

* feat(mvp): centralize resolution surfaces + fix SDK roadmap mode parity (#3178)

Three new SDK query verbs replace the architectural duplication surfaced by
the v1.50.0-canary.2 review against dev tip 12c4e565:

  phase.mvp-mode <N> [--cli-flag]
    Single canonical precedence resolver (CLI flag -> ROADMAP **Mode:** mvp
    -> workflow.mvp_mode config -> false). Replaces 4-8 lines of bash that
    were duplicated across plan-phase.md, execute-phase.md, verify-work.md,
    and progress.md. Returns {active, source, roadmap_mode, config_mvp_mode,
    cli_flag_present}.

  task.is-behavior-adding <plan-file> | --task-content <xml>
    Behavior-Adding Task predicate (tdd="true" + <behavior> block + non-test
    source files in <files>). Replaces prose-only specification in
    references/execute-mvp-tdd.md; gsd-executor agent now invokes the verb
    instead of re-inlining the three checks. Returns {is_behavior_adding,
    checks, reason}.

  user-story.validate <text> | --story <text>
    Owns the canonical User Story regex /^As a .+, I want to .+, so that .+\.$/
    previously hardcoded in verify-work.md prose. Consumed by gsd-verifier
    (phase-goal guard) and /gsd-mvp-phase (interactive-prompt validation).
    Returns {valid, slots: {role, capability, outcome}, errors[]}.

Bug fix bundled: sdk/src/query/roadmap.ts searchPhaseInContent now extracts
the mode field from **Mode:**, restoring parity with roadmap.cjs:120-123.
Without this, roadmap.get-phase --pick mode returned null on the native
dispatch path even when the phase had **Mode:** mvp set, causing MVP_MODE
to silently fall through to the config/false branch in every consuming
workflow. The original PRs Phase 1 (#2885) shipped the CJS parser but the
SDK port omitted the field; this fix brings them back to parity.

Workflows + agents updated to call the verbs:
  - plan-phase.md, execute-phase.md, verify-work.md, progress.md call
    phase.mvp-mode (one line replaces the duplicated bash chains).
  - execute-phase.md MVP+TDD gate calls task.is-behavior-adding.
  - verify-work.md goal guard calls user-story.validate.
  - mvp-phase.md interactive prompt validates via user-story.validate.
  - gsd-executor agent references task.is-behavior-adding instead of prose.
  - gsd-verifier agent references user-story.validate instead of inlined regex.

Tests: 24 new vitest tests in sdk/src/query/mvp.test.ts cover all three
verbs + the regression. Two existing contract tests (progress, verify)
updated to assert on the new verb shape. All 60 existing MVP contract
tests pass; golden integration suite (38 + 42 tests) passes.

Closes #3177

* fix(canary.2): unblock release gates for v1.50.0-canary.2

Run 25451329660 (Release SDK Bundle on dev, 2026-05-06T17:41) failed at the
test-suite step with 3 deterministic content/structure gate failures, all
attributable to the MVP umbrella integration in #3178 and the docs sweep
in #3180.

Failure 1: /gsd-mvp-phase undocumented in workflows/help.md
  - tests/bug-2954-help-md-slash-command-stubs.test.cjs requires every
    shipped commands/gsd/<X>.md to have a /gsd-<X> mention in help.md
  - PR #3180 updated docs/COMMANDS.md but missed help.md (which the AI
    agents load in-product)
  - Fix: add a /gsd-mvp-phase entry to help.md right before /gsd-plan-phase

Failures 2 + 3: execute-phase.md (1727) and plan-phase.md (1714) over XL budget (1700)
  - PR #3178 added MVP-mode verb calls (phase.mvp-mode, task.is-behavior-adding,
    user-story.validate) to both workflow files, pushing them past 1700 lines
  - Fix: bump XL_BUDGET 1700 -> 1800 with inline comment pointing at the
    structural follow-up (extract MVP bodies to <workflow>/modes/mvp.md per
    the discuss-phase/modes/ precedent)
  - The structural extract is the right long-term fix but is bigger than
    canary unblock scope; will land in a follow-up after canary cycles

Local verification:
  $ node --test tests/bug-2954-help-md-slash-command-stubs.test.cjs                 tests/workflow-size-budget.test.cjs
  tests 111  pass 111  fail 0

After this lands, re-trigger Release SDK Bundle on dev for v1.50.0-canary.2.

* chore(changeset): set PR number for canary.2 unblock

* fix(codex): generate-claude-md writes to AGENTS.md on Codex runtime

When config.runtime === 'codex' or GSD_RUNTIME=codex, override the
output target to AGENTS.md regardless of claude_md_path, so Codex
projects no longer have GSD sections written to CLAUDE.md by mistake.

Fixes both the CJS (gsd-tools) and SDK (profile-output.ts) paths.
Explicit --output flags are still honoured in both paths.

Closes #3163

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(plan-phase): remove agent: directive that caused OpenCode subagent dispatch

On OpenCode, any command with `agent: <name>` in its frontmatter is
auto-dispatched to a subagent context where the Agent tool is unavailable.
plan-phase.md and mvp-phase.md both carried `agent: gsd-planner`, causing
them to run inside gsd-planner's subagent context with no ability to spawn
researcher/planner/checker subagents — the orchestrator fell back to inline
execution for all three phases.

Fix: remove `agent: gsd-planner` from both command files so they run in the
main agent context. Also replace the stale `Task` tool in allowed-tools with
`Agent` (the correct dispatcher tool name post-#3168 rename).

Adds a structural regression test that parses YAML frontmatter of every
commands/gsd/*.md file and asserts no command carries an `agent:` directive.

Closes #3156

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(mvp): address CodeRabbit workflow and contract findings

* fix(execute-phase): use registered state.update query command

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 21:51:38 -04:00

1096 lines
47 KiB
JavaScript

/**
* 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, loadConfig } = 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.',
skills: 'No project skills found. Add skills to any of: `.claude/skills/`, `.agents/skills/`, `.cursor/skills/`, `.github/skills/`, or `.codex/skills/` with a `SKILL.md` index file.',
};
// Directories where project skills may live (checked in order)
const SKILL_SEARCH_DIRS = ['.claude/skills', '.agents/skills', '.cursor/skills', '.github/skills', '.codex/skills'];
const CLAUDE_MD_WORKFLOW_ENFORCEMENT = [
'Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.',
'',
'Use these entry points:',
'- `/gsd-quick` for small fixes, doc updates, and ad-hoc tasks',
'- `/gsd-debug` for investigation and bug fixing',
'- `/gsd-execute-phase` for planned phase work',
'',
'Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.',
].join('\n');
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', linkPath: null, 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', linkPath: null, hasFallback: true };
}
return { content: parts.join('\n\n'), source: 'PROJECT.md', linkPath: '.planning/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';
let linkPath = '.planning/codebase/STACK.md';
if (!content) {
content = safeReadFile(researchPath);
source = 'research/STACK.md';
linkPath = '.planning/research/STACK.md';
}
if (!content) {
return { content: CLAUDE_MD_FALLBACKS.stack, source: 'STACK.md', linkPath: null, 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, linkPath, 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', linkPath: null, 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', linkPath: '.planning/codebase/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', linkPath: null, 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', linkPath: '.planning/codebase/ARCHITECTURE.md', hasFallback: false };
}
function generateWorkflowSection() {
return {
content: CLAUDE_MD_WORKFLOW_ENFORCEMENT,
source: 'GSD defaults',
linkPath: null,
hasFallback: false,
};
}
/**
* Discover project skills from standard directories and extract frontmatter
* (name + description) for each. Returns a table summary for CLAUDE.md so
* agents know which skills are available at session startup (Layer 1 discovery).
*/
function generateSkillsSection(cwd) {
const discovered = [];
for (const dir of SKILL_SEARCH_DIRS) {
const absDir = path.join(cwd, dir);
if (!fs.existsSync(absDir)) continue;
let entries;
try {
entries = fs.readdirSync(absDir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
// Skip GSD's own installed skills — only surface project-specific skills
if (entry.name.startsWith('gsd-')) continue;
const skillMdPath = path.join(absDir, entry.name, 'SKILL.md');
if (!fs.existsSync(skillMdPath)) continue;
const content = safeReadFile(skillMdPath);
if (!content) continue;
const frontmatter = extractSkillFrontmatter(content);
const name = frontmatter.name || entry.name;
const description = frontmatter.description || '';
// Avoid duplicates when same skill dir is symlinked from multiple locations
if (discovered.some(s => s.name === name)) continue;
discovered.push({ name, description, path: `${dir}/${entry.name}` });
}
}
if (discovered.length === 0) {
return { content: CLAUDE_MD_FALLBACKS.skills, source: 'skills/', hasFallback: true };
}
const lines = ['| Skill | Description | Path |', '|-------|-------------|------|'];
for (const skill of discovered) {
// Sanitize table cell content (escape pipes)
const desc = skill.description.replace(/\|/g, '\\|').replace(/\n/g, ' ').trim();
const safeName = skill.name.replace(/\|/g, '\\|');
lines.push(`| ${safeName} | ${desc} | \`${skill.path}/SKILL.md\` |`);
}
return { content: lines.join('\n'), source: 'skills/', hasFallback: false };
}
/**
* Extract name and description from YAML-like frontmatter in a SKILL.md file.
* Handles multi-line description values (continuation lines indented with spaces).
*/
function extractSkillFrontmatter(content) {
const result = { name: '', description: '' };
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
if (!fmMatch) return result;
const fmBlock = fmMatch[1];
const lines = fmBlock.split('\n');
let currentKey = '';
for (const line of lines) {
// Top-level key: value
const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)/);
if (kvMatch) {
currentKey = kvMatch[1];
const value = kvMatch[2].trim();
if (currentKey === 'name') result.name = value;
if (currentKey === 'description') result.description = value;
continue;
}
// Continuation line (indented) for multi-line values
if (currentKey === 'description' && /^\s+/.test(line)) {
result.description += ' ' + line.trim();
} else {
currentKey = '';
}
}
return result;
}
// ─── 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 || analysis.projects_analyzed || []).join(', '));
template = template.replace(/\{\{message_count\}\}/g, String(analysis.message_count || analysis.messages_analyzed || 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 || analysis.projects_analyzed || []).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 = '';
const evidenceArr = dim.evidence_quotes || dim.evidence;
if (evidenceArr && Array.isArray(evidenceArr) && evidenceArr.length > 0) {
const evidenceLines = evidenceArr.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);
// #2973: v1.39.0's skills-only migration removed the legacy
// commands/gsd subdirectory in favor of skills/<skill>/SKILL.md under
// the runtime config dir. This writer was missed in the migration
// (PR #1540 targeted GSD-shipped command files; dev-preferences is a
// runtime-generated user artifact). Default now points at the skills/
// location so /gsd-profile-user --refresh stops re-creating the legacy
// directory. The path is constructed via path.join (not a literal
// string) so the cline-install leaked-path lint does not flag it.
let outputPath = options.output;
if (!outputPath) {
outputPath = path.join(os.homedir(), '.claude', 'skills', 'gsd-dev-preferences', 'SKILL.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 {
// Read claude_md_path from config, default to ./CLAUDE.md
let configClaudeMdPath = './CLAUDE.md';
try {
const config = loadConfig(cwd);
if (config.claude_md_path) configClaudeMdPath = config.claude_md_path;
} catch { /* use default */ }
targetPath = path.isAbsolute(configClaudeMdPath) ? configClaudeMdPath : path.join(cwd, configClaudeMdPath);
}
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', 'skills', 'workflow'];
const generators = {
project: generateProjectSection,
stack: generateStackSection,
conventions: generateConventionsSection,
architecture: generateArchitectureSection,
skills: generateSkillsSection,
workflow: generateWorkflowSection,
};
const sectionHeadings = {
project: '## Project',
stack: '## Technology Stack',
conventions: '## Conventions',
architecture: '## Architecture',
skills: '## Project Skills',
workflow: '## GSD Workflow Enforcement',
};
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 assemblyConfig = {};
let configClaudeMdPath = './CLAUDE.md';
try {
const config = loadConfig(cwd);
if (config.claude_md_path) configClaudeMdPath = config.claude_md_path;
if (config.claude_md_assembly) assemblyConfig = config.claude_md_assembly;
// #3163: When runtime is codex, override the output target to AGENTS.md
// regardless of claude_md_path, so Codex projects never write to CLAUDE.md.
// GSD_RUNTIME env var takes precedence over config.runtime, mirroring detectRuntime().
const effectiveRuntime = process.env.GSD_RUNTIME || config.runtime || null;
if (!options.output && effectiveRuntime === 'codex') {
configClaudeMdPath = './AGENTS.md';
}
} catch { /* use default */ }
let outputPath = options.output;
if (!outputPath) {
outputPath = path.isAbsolute(configClaudeMdPath) ? configClaudeMdPath : path.join(cwd, configClaudeMdPath);
} else if (!path.isAbsolute(outputPath)) {
outputPath = path.join(cwd, outputPath);
}
const globalAssemblyMode = assemblyConfig.mode || 'embed';
const blockModes = assemblyConfig.blocks || {};
// Return the assembled content for a section, respecting link vs embed mode.
// "link" mode writes `@<linkPath>` when the generator has a real source file.
// Falls back to "embed" for sections without a linkable source (workflow, fallbacks).
function buildSectionContent(name, gen, heading) {
const effectiveMode = blockModes[name] || globalAssemblyMode;
if (effectiveMode === 'link' && gen.linkPath && !gen.hasFallback) {
return buildSection(name, gen.source, `${heading}\n\n@${gen.linkPath}`);
}
return buildSection(name, gen.source, `${heading}\n\n${gen.content}`);
}
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];
sections.push(buildSectionContent(name, gen, heading));
}
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 fullSection = buildSectionContent(name, gen, heading);
const hasMarkers = fileContent.indexOf(`<!-- GSD:${name}-start`) !== -1;
if (hasMarkers) {
if (options.auto) {
const effectiveMode = blockModes[name] || globalAssemblyMode;
const expectedBody = (effectiveMode === 'link' && gen.linkPath && !gen.hasFallback)
? `${heading}\n\n@${gen.linkPath}`
: `${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,
};