Compare commits

11 Commits

Author SHA1 Message Date
local
956ec243c5 fix: update export bundle for Azure OpenAI and add streaming diagnostics
Replace CLIProxyAPI/local proxy references with Azure OpenAI using
DefaultAzureCredential and tenant ID auth. Add Critical Pattern #8
for SSE buffering diagnostics with timestamped curl test. Add
streaming verification tasks (T6b, T15) and troubleshooting entries
for Azure AD auth, RBAC, response compression, and proxy buffering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 01:42:38 +01:00
local
d46b179221 feat: add porting-guide skill and NL XVA Pricer export bundle
Add /opsx:porting-guide skill that generates detailed human-readable
implementation guides as a companion to /opsx:export-spec. The AI spec
targets the agent; the porting guide targets the human developer with
design rationale, task-by-task notes, troubleshooting tables, and
rollback plans.

Generate the full NL XVA Pricer export bundle for CRC:
- nlxva-pricer-spec.md (AI-targeted portable spec)
- nlxva-pricer-openspec.md (OpenSpec proposal/design/tasks)
- nlxva-pricer-porting-guide.md (human implementation guide)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 00:45:47 +01:00
local
5b027eb0db feat: add extraction schema, sidebar nav, few-shot prompting, and prompt settings
Overhaul extraction pipeline with new TradeItem model, conversation flow,
and dedicated extraction endpoint. Add sidebar navigation with NavMenu
component and landing page. Introduce few-shot prompting service and
tests. Add prompt settings and email upload specs. Update OpenSpec
tooling with improved export-spec and extract-feature commands. Archive
completed changes and export full specs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:39:23 +01:00
local
7a5c22593a feat: enable rich text display for assistant messages
Add markdown-to-HTML rendering for assistant messages using Markdig with
HTML sanitization. Includes cached rendering to avoid lag during streaming,
styled markdown elements (code blocks, tables, lists, blockquotes) within
chat bubbles, and 18 unit tests covering rendering and XSS prevention.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:29:58 +01:00
local
d3300c7db9 feat: add extract-feature and export-spec portability skills
Two new OpenSpec skills for porting features to sandboxed codebases:
- /opsx:extract-feature generates minimal, printable code recipes
- /opsx:export-spec generates compact specs for AI-assisted reimplementation
Both support cumulative dependency analysis across archived changes.

Includes first export of migrate-to-semantic-kernel in all three formats:
code recipe (~120 lines), portable spec (~40 lines), OpenSpec variant (~25 lines).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:59:06 +01:00
local
471e9ce935 feat: migrate chat backend to Semantic Kernel with tool calling support
Replace manual HTTP proxy in ChatController with Semantic Kernel's
OpenAI chat completion service pointed at CLIProxyAPI. Add extraction
plugin with validation function for structured field extraction from
natural language, enabling an agentic loop with auto-retry and
human-in-the-loop escalation.

- Add Microsoft.SemanticKernel 1.74.0 with OpenAI connector
- Create ExtractedFields schema and ValidationResult models
- Create ExtractionPlugin with [KernelFunction] validation
- Rewrite ChatController to use IChatCompletionService streaming
- Configure FunctionChoiceBehavior.Auto() for tool calling
- Preserve existing SSE contract (client unchanged)
- Update tests to mock SK services, add plugin and integration tests
- Archive multi-turn-conversations and migrate-to-semantic-kernel changes
- Sync specs for agent-extraction, semantic-kernel-integration, chat-streaming

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:59:13 +01:00
local
3278a408b9 feat: add multi-turn conversation support within a session
Send full conversation history with each API request so the AI maintains
context across exchanges. Add "New Chat" button to clear the conversation
and start fresh. No persistent storage — session resets on page reload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 02:40:33 +01:00
local
17a5a58e73 test: add xUnit test coverage for API controllers and client services
Create ChatAgent.Api.Tests and ChatAgent.Client.Tests projects with xUnit
and Moq. Test HealthController (200 + valid response), ChatController
(SSE streaming with mocked upstream, error handling), and ChatApiClient
(delta parsing, error events, health endpoint). 6 tests, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 02:21:10 +01:00
local
00e7df2802 feat: wire chat UI to Responses API with streaming
Add ChatController that proxies POST /api/chat to the local Responses API
(localhost:8317/v1/responses) with SSE streaming. Client reads tokens via
SetBrowserResponseStreamingEnabled and renders them incrementally. Includes
thinking indicator, input disabled during streaming, and error handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:54:28 +01:00
local
1614a61617 feat: add basic chat interface with MudBlazor and propose responses API integration
Install MudBlazor 9.2.0, replace Bootstrap layout with MudLayout/MudAppBar,
create Chat.razor with message list, text input, auto-scroll, and hardcoded
responses. Add ChatMessage shared model. Remove template pages (Counter,
Weather), move health check to /health. Include OpenSpec change artifacts
for the upcoming wire-responses-api work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:24:40 +01:00
local
a462b7dbc7 feat: migrate CLAUDE.md content to OpenSpec specs and initialize workflow
Move project description and tech stack research from CLAUDE.md into
openspec/specs/project/ and openspec/specs/stack/. Slim CLAUDE.md to a
pointer file. Populate config.yaml with project context. Add OpenSpec
CLI skills and commands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:23:13 +01:00
174 changed files with 13532 additions and 540 deletions

View File

@@ -0,0 +1,242 @@
---
name: "OPSX: Bulk Archive"
description: Archive multiple completed changes at once
category: Workflow
tags: [workflow, archive, experimental, bulk]
---
Archive multiple completed changes in a single operation.
This skill allows you to batch-archive changes, handling spec conflicts intelligently by checking the codebase to determine what's actually implemented.
**Input**: None required (prompts for selection)
**Steps**
1. **Get active changes**
Run `openspec list --json` to get all active changes.
If no active changes exist, inform user and stop.
2. **Prompt for change selection**
Use **AskUserQuestion tool** with multi-select to let user choose changes:
- Show each change with its schema
- Include an option for "All changes"
- Allow any number of selections (1+ works, 2+ is the typical use case)
**IMPORTANT**: Do NOT auto-select. Always let the user choose.
3. **Batch validation - gather status for all selected changes**
For each selected change, collect:
a. **Artifact status** - Run `openspec status --change "<name>" --json`
- Parse `schemaName` and `artifacts` list
- Note which artifacts are `done` vs other states
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
- If no tasks file exists, note as "No tasks"
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
- List which capability specs exist
- For each, extract requirement names (lines matching `### Requirement: <name>`)
4. **Detect spec conflicts**
Build a map of `capability -> [changes that touch it]`:
```
auth -> [change-a, change-b] <- CONFLICT (2+ changes)
api -> [change-c] <- OK (only 1 change)
```
A conflict exists when 2+ selected changes have delta specs for the same capability.
5. **Resolve conflicts agentically**
**For each conflict**, investigate the codebase:
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
b. **Search the codebase** for implementation evidence:
- Look for code implementing requirements from each delta spec
- Check for related files, functions, or tests
c. **Determine resolution**:
- If only one change is actually implemented -> sync that one's specs
- If both implemented -> apply in chronological order (older first, newer overwrites)
- If neither implemented -> skip spec sync, warn user
d. **Record resolution** for each conflict:
- Which change's specs to apply
- In what order (if both)
- Rationale (what was found in codebase)
6. **Show consolidated status table**
Display a table summarizing all changes:
```
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|---------------------|-----------|-------|---------|-----------|--------|
| schema-management | Done | 5/5 | 2 delta | None | Ready |
| project-config | Done | 3/3 | 1 delta | None | Ready |
| add-oauth | Done | 4/4 | 1 delta | auth (!) | Ready* |
| add-verify-skill | 1 left | 2/5 | None | None | Warn |
```
For conflicts, show the resolution:
```
* Conflict resolution:
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
```
For incomplete changes, show warnings:
```
Warnings:
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
```
7. **Confirm batch operation**
Use **AskUserQuestion tool** with a single confirmation:
- "Archive N changes?" with options based on status
- Options might include:
- "Archive all N changes"
- "Archive only N ready changes (skip incomplete)"
- "Cancel"
If there are incomplete changes, make clear they'll be archived with warnings.
8. **Execute archive for each confirmed change**
Process changes in the determined order (respecting conflict resolution):
a. **Sync specs** if delta specs exist:
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
- For conflicts, apply in resolved order
- Track if sync was done
b. **Perform the archive**:
```bash
mkdir -p openspec/changes/archive
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
```
c. **Track outcome** for each change:
- Success: archived successfully
- Failed: error during archive (record error)
- Skipped: user chose not to archive (if applicable)
9. **Display summary**
Show final results:
```
## Bulk Archive Complete
Archived 3 changes:
- schema-management-cli -> archive/2026-01-19-schema-management-cli/
- project-config -> archive/2026-01-19-project-config/
- add-oauth -> archive/2026-01-19-add-oauth/
Skipped 1 change:
- add-verify-skill (user chose not to archive incomplete)
Spec sync summary:
- 4 delta specs synced to main specs
- 1 conflict resolved (auth: applied both in chronological order)
```
If any failures:
```
Failed 1 change:
- some-change: Archive directory already exists
```
**Conflict Resolution Examples**
Example 1: Only one implemented
```
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
Checking add-oauth:
- Delta adds "OAuth Provider Integration" requirement
- Searching codebase... found src/auth/oauth.ts implementing OAuth flow
Checking add-jwt:
- Delta adds "JWT Token Handling" requirement
- Searching codebase... no JWT implementation found
Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
```
Example 2: Both implemented
```
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
Checking add-rest-api (created 2026-01-10):
- Delta adds "REST Endpoints" requirement
- Searching codebase... found src/api/rest.ts
Checking add-graphql (created 2026-01-15):
- Delta adds "GraphQL Schema" requirement
- Searching codebase... found src/api/graphql.ts
Resolution: Both implemented. Will apply add-rest-api specs first,
then add-graphql specs (chronological order, newer takes precedence).
```
**Output On Success**
```
## Bulk Archive Complete
Archived N changes:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
- <change-2> -> archive/YYYY-MM-DD-<change-2>/
Spec sync summary:
- N delta specs synced to main specs
- No conflicts (or: M conflicts resolved)
```
**Output On Partial Success**
```
## Bulk Archive Complete (partial)
Archived N changes:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
Skipped M changes:
- <change-2> (user chose not to archive incomplete)
Failed K changes:
- <change-3>: Archive directory already exists
```
**Output When No Changes**
```
## No Changes to Archive
No active changes found. Create a new change to get started.
```
**Guardrails**
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
- Always prompt for selection, never auto-select
- Detect spec conflicts early and resolve by checking codebase
- When both changes are implemented, apply specs in chronological order
- Skip spec sync only when implementation is missing (warn user)
- Show clear per-change status before confirming
- Use single confirmation for entire batch
- Track and report all outcomes (success/skip/fail)
- Preserve .openspec.yaml when moving to archive
- Archive directory target uses current date: YYYY-MM-DD-<name>
- If archive target exists, fail that change but continue with others

View File

@@ -0,0 +1,114 @@
---
name: "OPSX: Continue"
description: Continue working on a change - create the next artifact (Experimental)
category: Workflow
tags: [workflow, artifacts, experimental]
---
Continue working on a change by creating the next artifact.
**Input**: Optionally specify a change name after `/opsx:continue` (e.g., `/opsx:continue add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes sorted by most recently modified. Then use the **AskUserQuestion tool** to let the user select which change to work on.
Present the top 3-4 most recently modified changes as options, showing:
- Change name
- Schema (from `schema` field if present, otherwise "spec-driven")
- Status (e.g., "0/5 tasks", "complete", "no tasks")
- How recently it was modified (from `lastModified` field)
Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to continue.
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check current status**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand current state. The response includes:
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
- `isComplete`: Boolean indicating if all artifacts are complete
3. **Act based on status**:
---
**If all artifacts are complete (`isComplete: true`)**:
- Congratulate the user
- Show final status including the schema used
- Suggest: "All artifacts created! You can now implement this change with `/opsx:apply` or archive it with `/opsx:archive`."
- STOP
---
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
- Pick the FIRST artifact with `status: "ready"` from the status output
- Get its instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- Parse the JSON. The key fields are:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- **Create the artifact file**:
- Read any completed dependency files for context
- Use `template` as the structure - fill in its sections
- Apply `context` and `rules` as constraints when writing - but do NOT copy them into the file
- Write to the output path specified in instructions
- Show what was created and what's now unlocked
- STOP after creating ONE artifact
---
**If no artifacts are ready (all blocked)**:
- This shouldn't happen with a valid schema
- Show status and suggest checking for issues
4. **After creating an artifact, show progress**
```bash
openspec status --change "<name>"
```
**Output**
After each invocation, show:
- Which artifact was created
- Schema workflow being used
- Current progress (N/M complete)
- What artifacts are now unlocked
- Prompt: "Run `/opsx:continue` to create the next artifact"
**Artifact Creation Guidelines**
The artifact types and their purpose depend on the schema. Use the `instruction` field from the instructions output to understand what to create.
Common artifact patterns:
**spec-driven schema** (proposal → specs → design → tasks):
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
- The Capabilities section is critical - each capability listed will need a spec file.
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
- **design.md**: Document technical decisions, architecture, and implementation approach.
- **tasks.md**: Break down implementation into checkboxed tasks.
For other schemas, follow the `instruction` field from the CLI output.
**Guardrails**
- Create ONE artifact per invocation
- Always read dependency artifacts before creating a new one
- Never skip artifacts or create out of order
- If context is unclear, ask the user before creating
- Verify the artifact file exists after writing before marking progress
- Use the schema's artifact sequence, don't assume specific artifact names
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output

View File

@@ -0,0 +1,227 @@
---
name: "OPSX: Export Spec"
description: Export a feature as a compact, portable spec for AI-assisted reimplementation on a sandboxed machine
category: Workflow
tags: [workflow, portability, experimental]
---
Export a feature as a portable spec for AI-assisted reimplementation.
Instead of retyping code, you retype a compact spec. The AI on the sandbox generates the code.
---
**Input**: The argument after `/opsx:export-spec` is a change name (active or archived), or a description of the feature. If omitted, prompt for selection.
**Steps**
1. **Identify the source feature**
Same selection logic as `/opsx:extract-feature`:
- Check active changes and archive for the change name
- If not found, prompt with **AskUserQuestion tool**
- Read all artifacts: `proposal.md`, `design.md`, `tasks.md`, specs
2. **Analyze dependency chain (cumulative mode)**
Features often build on each other. Before generating the spec, determine
what the target feature depends on.
a. Read all archived change proposals in `openspec/changes/archive/` (sorted by date).
b. Build a dependency graph: what does the target require? What's superseded?
c. Ask the user:
> "This feature depends on earlier changes. How should I scope the spec?"
> 1. **Cumulative** — include all foundation (recommended for fresh codebase)
> 2. **Delta only** — just this change (target already has foundation)
> 3. **Custom** — pick which dependencies to include
d. In cumulative mode: merge into one coherent spec, skip superseded components.
e. In delta mode: add an "Assumes" section listing what must already exist.
3. **Read the actual implementation**
Read all source files created or modified by this feature (and dependencies if cumulative).
The spec must reflect what was actually built, not just what was planned.
4. **Determine the target context**
Use **AskUserQuestion tool** to ask:
> "Tell me about the target codebase:
> 1. Project name / root namespace
> 2. Existing stack (ASP.NET Core? Blazor? MudBlazor?)
> 3. Does it already have any of these? (controllers, DI setup, chat endpoint)
> 4. Does the target have OpenSpec? GitHub Copilot? Claude Code?"
This shapes what the spec assumes vs what it must specify.
5. **Capture the target GUI layout**
If the feature has a UI component, the spec MUST include the target's layout context.
Without it, the AI will generate components that conflict with the existing shell.
Ask the user:
> "Does the target app have an existing GUI shell? If so, share:
> 1. A **screenshot** (drag/drop or file path) — most information-dense
> 2. Or describe: AppBar (height? Dense?), Sidebar/Drawer (width? toggled?),
> MainContent area, any fixed elements"
If a screenshot is provided, extract:
- AppBar type and height (Dense=48px, Regular=64px, custom)
- Sidebar/Drawer presence, width, toggle behavior
- Main content area constraints
- Existing navigation items
Then ask:
> "How should this feature integrate into the target?
> 1. Feature name in the target (e.g., 'Sales Assistant' not 'Chat')
> 2. Route path (e.g., `/sales-assistant`)
> 3. Navigation: new sidebar item? AppBar button? Floating panel?"
Generate an **ASCII layout diagram** showing exactly where the feature renders,
and include it in both the spec and the OpenSpec design.md.
6. **Generate the portable spec**
Create a single markdown document that is:
- **Compact**: Target ~30-50 lines for a medium feature
- **Precise**: Unambiguous enough for an AI to implement correctly
- **Self-contained**: No references to external files or repos
- **Stack-aware**: Uses the right terminology for the target stack
Structure:
```markdown
# Feature: <Name>
## Target: <project name> (<stack>)
## Assumes
<what must already exist on the target>
## Integration Rule
This feature is additive only. DO NOT modify existing files, components,
services, or patterns in the target. If the target already has an equivalent
service, use it instead of creating a new one. If a task conflicts with
existing target code, skip it and note the conflict. Existing applicationX
code takes precedence over this spec in all cases.
## Target Layout
<ASCII diagram showing the target app shell and where this feature renders>
<AppBar height, Sidebar width, MainContent constraints>
<Feature name, route, navigation integration point>
## Packages
<list with versions>
## Architecture
<2-3 sentence overview>
## Components
### <Component>: <path hint>
- <What it does>
- <Key behavior>
- <Interface/contract>
## Contracts
<API shapes, model definitions — things that MUST be exact>
## Critical Patterns
<Exact code snippets for traps the AI will otherwise get wrong>
<Show the POSITIVE pattern to copy, not just "don't use X">
<Include WHY — the mechanism behind the trap>
## Wiring
<DI registration, middleware order, config keys — dependency order>
## Behavior
<Non-obvious requirements>
```
**Compression strategies:**
- Use bullet points, not prose
- Specify contracts precisely (field names, types, API shapes)
- Let the AI infer standard patterns
- Only specify non-obvious behavior
- Omit anything the AI would do by default
7. **Estimate typing effort**
Count characters in the spec. Compare to the code recipe equivalent.
Show the compression ratio.
8. **Optionally generate an OpenSpec-compatible version**
If the target has OpenSpec, also generate:
- A **config.yaml** template — adapted context for the target project, with placeholders
- A `proposal.md` (minimal — 5-10 lines)
- A `design.md` — compact architectural decisions and rationale (why SSE vs WebSocket,
why typed client vs raw, why per-request plugin import, etc.). Without this the AI
has tasks but no rationale, and will guess wrong on non-obvious decisions.
- A `tasks.md` (implementation steps)
- **Setup instructions** at the top: step-by-step procedure for the target machine
(create config.yaml, save proposal/design/tasks, run `/opsx:apply`, reference the portable spec)
Save as: `openspec/exports/<change-name>-openspec.md`
9. **Write the output**
Save to: `openspec/exports/<change-name>-spec.md`
Display the full content for review.
**Guardrails**
- Prioritize precision over brevity — ambiguity wastes more time than length
- Always include exact field names, types, and API shapes
- Include non-obvious gotchas (like /v1 base URL requirements)
- Mental test: could an AI implement this correctly without seeing the original code?
- If too complex for ~50 lines, split into multiple specs by component
- Always show the compression ratio
- Must be readable when printed in monospace — no wide tables or long lines
- In cumulative mode, the spec must read as one coherent feature, not a list of changes
- Skip superseded components — always describe the latest version
- In delta mode, add an "Assumes" section so the target AI knows what must exist
- In the output header, note which changes were included and which were skipped
**Integration rule: adapt, never modify**
The exported feature is a GUEST in the target application. Existing code, patterns,
and conventions in the target take absolute precedence. The spec must include an
explicit "Integration Rule" section that states:
- **DO NOT** modify existing files, components, layouts, services, or routing in the target
- **DO NOT** replace existing patterns with patterns from the source project
(e.g., if the target uses a different HttpClient pattern, use theirs)
- **DO** add new files, new nav links, new routes, new DI registrations
- **DO** conform to the target's existing code style, naming conventions, and project structure
- If a task conflicts with existing target code, **stop and notify the user** — the user decides whether to skip, adapt, or redesign
- If the target already has an equivalent service (e.g., its own markdown renderer,
HttpClient wrapper), **use the existing one** instead of creating a new one
When generating the spec, actively identify potential conflict points and add
explicit "Adapt to target" notes. Common conflicts:
- Program.cs / DI registration style
- HttpClient patterns (typed client vs named client vs raw)
- Layout components (don't restructure MainLayout)
- CSS approach (isolation vs global vs utility classes)
- Error handling patterns
- Navigation structure
**Anti-patterns learned from field use**
These patterns cause the target AI to deviate from the spec even when the spec mentions them:
1. **"Don't use X" warnings get ignored.** AIs skip parenthetical negations buried in
bullet lists because their training data overwhelmingly uses the standard pattern.
Instead: add a **"Critical Patterns"** section with the exact code snippet to copy.
Show the positive pattern, not just the negative warning. E.g., instead of
"don't use EndOfStream", show the exact `while ((line = await ReadLineAsync()) != null)` loop.
2. **Layout values that depend on other components break silently.** Magic numbers like
`calc(100vh - 48px)` assume a specific AppBar height. Instead: document the dependency
explicitly ("48px = MudAppBar Dense height"), suggest using a CSS variable as fallback,
and note mobile viewport gotchas (`100vh` vs `100dvh`).
3. **Platform-specific runtime traps need the WHY.** Saying "don't use EndOfStream" is
not enough — the AI needs to know WHY (synchronous peek on an async-only stream).
When the AI understands the mechanism, it's less likely to reach for an equivalent
pattern that has the same problem.
ARGUMENTS: based on the above

View File

@@ -0,0 +1,114 @@
---
name: "OPSX: Extract Feature"
description: Extract a feature into a minimal, printable code recipe for manual reimplementation
category: Workflow
tags: [workflow, portability, experimental]
---
Extract a feature into a minimal, printable code recipe for manual reimplementation.
Generates a markdown document with:
- Package dependencies
- Ordered code blocks (no comments, no boilerplate)
- Clear markers for generic vs domain-specific code
Print it, take it to the sandbox, type it in.
---
**Input**: The argument after `/opsx:extract-feature` is a change name (active or archived), a git commit range, or a list of files. If omitted, prompt for selection.
**Steps**
1. **Identify the source feature**
If a change name is provided:
- Check active changes: `openspec list --json`
- Check archive: look in `openspec/changes/archive/` for directories ending with the name
- If found, read its artifacts: `proposal.md`, `design.md`, `tasks.md`
If no name provided:
- Run `openspec list --json` and list archived changes
- Use **AskUserQuestion tool** to let the user select
If a file list or commit range is provided instead:
- Read those files directly
- Identify the feature from the code
2. **Analyze dependency chain (cumulative mode)**
Features often build on each other. Before generating the recipe, determine
what the target feature depends on.
a. Read all archived change proposals in `openspec/changes/archive/` (sorted by date).
b. Build a dependency graph: what does the target require? What's superseded?
c. Ask the user:
> "This feature depends on earlier changes. How should I scope the recipe?"
> 1. **Cumulative** — include all foundation code (recommended for fresh codebase)
> 2. **Delta only** — just this change's code (target already has foundation)
> 3. **Custom** — pick which dependencies to include
d. In cumulative mode: merge the chain, skip superseded code, use final file versions.
e. In delta mode: include only the selected change's code.
3. **Analyze the feature scope**
From the change artifacts and/or code (across full dependency chain if cumulative), determine:
- Which files were created or modified
- What NuGet/npm packages were added
- What the dependency order is (e.g., models before controllers)
- What is generic infrastructure vs domain-specific logic
Read all relevant source files. Always read the final current version of each file.
4. **Generate the code recipe**
Create a markdown document with these sections, in this order:
**Header**: Feature name, source change, date
**Prerequisites**: Package references with exact versions
**Steps**: Ordered by dependency. Each step contains:
- Step number and action (e.g., "New file", "Add to existing file", "Modify")
- Target path (relative, adaptable)
- Namespace placeholder: `__YOUR_NAMESPACE__` where the target project namespace goes
- **Code block**: The actual code, stripped of:
- All comments (// and /* */ and /// XML docs)
- Redundant blank lines
- Verbose variable names that can be shortened
- Any code not directly related to the feature
- If the step modifies an existing file: show only the code to add, with a brief marker for insertion point (e.g., "Add after AddControllers()")
**Domain-Specific Sections**: Clearly marked with `// ADAPT:` prefix explaining what to change for the target domain
5. **Optimize for retyping**
Review the generated document and:
- Merge small files if they can be combined
- Remove any using statements that the IDE will auto-add
- Shorten any unnecessarily verbose code
- Ensure no step exceeds ~40 lines (split if needed)
- Add line counts per step so the user can estimate effort
- Total the overall line count at the top
6. **Write the output**
Save to: `openspec/exports/<change-name>-recipe.md`
Also display the full content so the user can review it immediately.
**Guardrails**
- Never include comments in code blocks — the goal is minimum keystrokes
- Always read the actual current source files, not just the change artifacts
- Preserve compilation order: models -> services -> controllers -> DI registration
- Mark domain-specific code clearly so the user knows what to adapt vs copy verbatim
- Keep each step self-contained — the user may take breaks between steps
- If a feature spans more than ~200 lines of stripped code, warn the user and suggest using `/opsx:export-spec` instead
- If the feature has UI components, warn that the code recipe does not capture layout context (AppBar height, sidebar width, container sizing). Suggest `/opsx:export-spec` for UI-heavy features — it includes a Target Layout section with ASCII diagram and integration guidance
- Output must be valid markdown that renders well when printed
- When in cumulative mode, skip superseded code — always use the latest version of each file
- In the output header, show which changes were included and which were skipped
- If a dependency chain is long (4+ changes), suggest `/opsx:export-spec` as more efficient
ARGUMENTS: based on the above

View File

@@ -0,0 +1,97 @@
---
name: "OPSX: Fast Forward"
description: Create a change and generate all artifacts needed for implementation in one go
category: Workflow
tags: [workflow, artifacts, experimental]
---
Fast-forward through artifact creation - generate everything needed to start implementation.
**Input**: The argument after `/opsx:ff` is the change name (kebab-case), OR a description of what the user wants to build.
**Steps**
1. **If no input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
```bash
openspec new change "<name>"
```
This creates a scaffolded change at `openspec/changes/<name>/`.
3. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts with their status and dependencies
4. **Create artifacts in sequence until apply-ready**
Use the **TodoWrite tool** to track progress through the artifacts.
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "✓ Created <artifact-id>"
b. **Continue until all `applyRequires` artifacts are complete**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
5. **Show final status**
```bash
openspec status --change "<name>"
```
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
- Prompt: "Run `/opsx:apply` to start implementing."
**Artifact Creation Guidelines**
- Follow the `instruction` field from `openspec instructions` for each artifact type
- The schema defines what each artifact should contain - follow it
- Read dependency artifacts for context before creating new ones
- Use `template` as the structure for your output file - fill in its sections
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
- Always read dependency artifacts before creating a new one
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
- If a change with that name already exists, ask if user wants to continue it or create a new one
- Verify each artifact file exists after writing before proceeding to next

View File

@@ -0,0 +1,69 @@
---
name: "OPSX: New"
description: Start a new change using the experimental artifact workflow (OPSX)
category: Workflow
tags: [workflow, artifacts, experimental]
---
Start a new change using the experimental artifact-driven approach.
**Input**: The argument after `/opsx:new` is the change name (kebab-case), OR a description of what the user wants to build.
**Steps**
1. **If no input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Determine the workflow schema**
Use the default schema (omit `--schema`) unless the user explicitly requests a different workflow.
**Use a different schema only if the user mentions:**
- A specific schema name → use `--schema <name>`
- "show workflows" or "what workflows" → run `openspec schemas --json` and let them choose
**Otherwise**: Omit `--schema` to use the default.
3. **Create the change directory**
```bash
openspec new change "<name>"
```
Add `--schema <name>` only if the user requested a specific workflow.
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
4. **Show the artifact status**
```bash
openspec status --change "<name>"
```
This shows which artifacts need to be created and which are ready (dependencies satisfied).
5. **Get instructions for the first artifact**
The first artifact depends on the schema. Check the status output to find the first artifact with status "ready".
```bash
openspec instructions <first-artifact-id> --change "<name>"
```
This outputs the template and context for creating the first artifact.
6. **STOP and wait for user direction**
**Output**
After completing the steps, summarize:
- Change name and location
- Schema/workflow being used and its artifact sequence
- Current status (0/N artifacts complete)
- The template for the first artifact
- Prompt: "Ready to create the first artifact? Run `/opsx:continue` or just describe what this change is about and I'll draft it."
**Guardrails**
- Do NOT create any artifacts yet - just show the instructions
- Do NOT advance beyond showing the first artifact template
- If the name is invalid (not kebab-case), ask for a valid name
- If a change with that name already exists, suggest using `/opsx:continue` instead
- Pass --schema if using a non-default workflow

View File

@@ -0,0 +1,550 @@
---
name: "OPSX: Onboard"
description: Guided onboarding - walk through a complete OpenSpec workflow cycle with narration
category: Workflow
tags: [workflow, onboarding, tutorial, learning]
---
Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step.
---
## Preflight
Before starting, check if the OpenSpec CLI is installed:
```bash
# Unix/macOS
openspec --version 2>&1 || echo "CLI_NOT_INSTALLED"
# Windows (PowerShell)
# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" }
```
**If CLI not installed:**
> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`.
Stop here if not installed.
---
## Phase 1: Welcome
Display:
```
## Welcome to OpenSpec!
I'll walk you through a complete change cycle—from idea to implementation—using a real task in your codebase. Along the way, you'll learn the workflow by doing it.
**What we'll do:**
1. Pick a small, real task in your codebase
2. Explore the problem briefly
3. Create a change (the container for our work)
4. Build the artifacts: proposal → specs → design → tasks
5. Implement the tasks
6. Archive the completed change
**Time:** ~15-20 minutes
Let's start by finding something to work on.
```
---
## Phase 2: Task Selection
### Codebase Analysis
Scan the codebase for small improvement opportunities. Look for:
1. **TODO/FIXME comments** - Search for `TODO`, `FIXME`, `HACK`, `XXX` in code files
2. **Missing error handling** - `catch` blocks that swallow errors, risky operations without try-catch
3. **Functions without tests** - Cross-reference `src/` with test directories
4. **Type issues** - `any` types in TypeScript files (`: any`, `as any`)
5. **Debug artifacts** - `console.log`, `console.debug`, `debugger` statements in non-debug code
6. **Missing validation** - User input handlers without validation
Also check recent git activity:
```bash
# Unix/macOS
git log --oneline -10 2>/dev/null || echo "No git history"
# Windows (PowerShell)
# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" }
```
### Present Suggestions
From your analysis, present 3-4 specific suggestions:
```
## Task Suggestions
Based on scanning your codebase, here are some good starter tasks:
**1. [Most promising task]**
Location: `src/path/to/file.ts:42`
Scope: ~1-2 files, ~20-30 lines
Why it's good: [brief reason]
**2. [Second task]**
Location: `src/another/file.ts`
Scope: ~1 file, ~15 lines
Why it's good: [brief reason]
**3. [Third task]**
Location: [location]
Scope: [estimate]
Why it's good: [brief reason]
**4. Something else?**
Tell me what you'd like to work on.
Which task interests you? (Pick a number or describe your own)
```
**If nothing found:** Fall back to asking what the user wants to build:
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
### Scope Guardrail
If the user picks or describes something too large (major feature, multi-day work):
```
That's a valuable task, but it's probably larger than ideal for your first OpenSpec run-through.
For learning the workflow, smaller is better—it lets you see the full cycle without getting stuck in implementation details.
**Options:**
1. **Slice it smaller** - What's the smallest useful piece of [their task]? Maybe just [specific slice]?
2. **Pick something else** - One of the other suggestions, or a different small task?
3. **Do it anyway** - If you really want to tackle this, we can. Just know it'll take longer.
What would you prefer?
```
Let the user override if they insist—this is a soft guardrail.
---
## Phase 3: Explore Demo
Once a task is selected, briefly demonstrate explore mode:
```
Before we create a change, let me quickly show you **explore mode**—it's how you think through problems before committing to a direction.
```
Spend 1-2 minutes investigating the relevant code:
- Read the file(s) involved
- Draw a quick ASCII diagram if it helps
- Note any considerations
```
## Quick Exploration
[Your brief analysis—what you found, any considerations]
┌─────────────────────────────────────────┐
│ [Optional: ASCII diagram if helpful] │
└─────────────────────────────────────────┘
Explore mode (`/opsx:explore`) is for this kind of thinking—investigating before implementing. You can use it anytime you need to think through a problem.
Now let's create a change to hold our work.
```
**PAUSE** - Wait for user acknowledgment before proceeding.
---
## Phase 4: Create the Change
**EXPLAIN:**
```
## Creating a Change
A "change" in OpenSpec is a container for all the thinking and planning around a piece of work. It lives in `openspec/changes/<name>/` and holds your artifacts—proposal, specs, design, tasks.
Let me create one for our task.
```
**DO:** Create the change with a derived kebab-case name:
```bash
openspec new change "<derived-name>"
```
**SHOW:**
```
Created: `openspec/changes/<name>/`
The folder structure:
```
openspec/changes/<name>/
├── proposal.md ← Why we're doing this (empty, we'll fill it)
├── design.md ← How we'll build it (empty)
├── specs/ ← Detailed requirements (empty)
└── tasks.md ← Implementation checklist (empty)
```
Now let's fill in the first artifact—the proposal.
```
---
## Phase 5: Proposal
**EXPLAIN:**
```
## The Proposal
The proposal captures **why** we're making this change and **what** it involves at a high level. It's the "elevator pitch" for the work.
I'll draft one based on our task.
```
**DO:** Draft the proposal content (don't save yet):
```
Here's a draft proposal:
---
## Why
[1-2 sentences explaining the problem/opportunity]
## What Changes
[Bullet points of what will be different]
## Capabilities
### New Capabilities
- `<capability-name>`: [brief description]
### Modified Capabilities
<!-- If modifying existing behavior -->
## Impact
- `src/path/to/file.ts`: [what changes]
- [other files if applicable]
---
Does this capture the intent? I can adjust before we save it.
```
**PAUSE** - Wait for user approval/feedback.
After approval, save the proposal:
```bash
openspec instructions proposal --change "<name>" --json
```
Then write the content to `openspec/changes/<name>/proposal.md`.
```
Proposal saved. This is your "why" document—you can always come back and refine it as understanding evolves.
Next up: specs.
```
---
## Phase 6: Specs
**EXPLAIN:**
```
## Specs
Specs define **what** we're building in precise, testable terms. They use a requirement/scenario format that makes expected behavior crystal clear.
For a small task like this, we might only need one spec file.
```
**DO:** Create the spec file:
```bash
# Unix/macOS
mkdir -p openspec/changes/<name>/specs/<capability-name>
# Windows (PowerShell)
# New-Item -ItemType Directory -Force -Path "openspec/changes/<name>/specs/<capability-name>"
```
Draft the spec content:
```
Here's the spec:
---
## ADDED Requirements
### Requirement: <Name>
<Description of what the system should do>
#### Scenario: <Scenario name>
- **WHEN** <trigger condition>
- **THEN** <expected outcome>
- **AND** <additional outcome if needed>
---
This format—WHEN/THEN/AND—makes requirements testable. You can literally read them as test cases.
```
Save to `openspec/changes/<name>/specs/<capability>/spec.md`.
---
## Phase 7: Design
**EXPLAIN:**
```
## Design
The design captures **how** we'll build it—technical decisions, tradeoffs, approach.
For small changes, this might be brief. That's fine—not every change needs deep design discussion.
```
**DO:** Draft design.md:
```
Here's the design:
---
## Context
[Brief context about the current state]
## Goals / Non-Goals
**Goals:**
- [What we're trying to achieve]
**Non-Goals:**
- [What's explicitly out of scope]
## Decisions
### Decision 1: [Key decision]
[Explanation of approach and rationale]
---
For a small task, this captures the key decisions without over-engineering.
```
Save to `openspec/changes/<name>/design.md`.
---
## Phase 8: Tasks
**EXPLAIN:**
```
## Tasks
Finally, we break the work into implementation tasks—checkboxes that drive the apply phase.
These should be small, clear, and in logical order.
```
**DO:** Generate tasks based on specs and design:
```
Here are the implementation tasks:
---
## 1. [Category or file]
- [ ] 1.1 [Specific task]
- [ ] 1.2 [Specific task]
## 2. Verify
- [ ] 2.1 [Verification step]
---
Each checkbox becomes a unit of work in the apply phase. Ready to implement?
```
**PAUSE** - Wait for user to confirm they're ready to implement.
Save to `openspec/changes/<name>/tasks.md`.
---
## Phase 9: Apply (Implementation)
**EXPLAIN:**
```
## Implementation
Now we implement each task, checking them off as we go. I'll announce each one and occasionally note how the specs/design informed the approach.
```
**DO:** For each task:
1. Announce: "Working on task N: [description]"
2. Implement the change in the codebase
3. Reference specs/design naturally: "The spec says X, so I'm doing Y"
4. Mark complete in tasks.md: `- [ ]``- [x]`
5. Brief status: "✓ Task N complete"
Keep narration light—don't over-explain every line of code.
After all tasks:
```
## Implementation Complete
All tasks done:
- [x] Task 1
- [x] Task 2
- [x] ...
The change is implemented! One more step—let's archive it.
```
---
## Phase 10: Archive
**EXPLAIN:**
```
## Archiving
When a change is complete, we archive it. This moves it from `openspec/changes/` to `openspec/changes/archive/YYYY-MM-DD-<name>/`.
Archived changes become your project's decision history—you can always find them later to understand why something was built a certain way.
```
**DO:**
```bash
openspec archive "<name>"
```
**SHOW:**
```
Archived to: `openspec/changes/archive/YYYY-MM-DD-<name>/`
The change is now part of your project's history. The code is in your codebase, the decision record is preserved.
```
---
## Phase 11: Recap & Next Steps
```
## Congratulations!
You just completed a full OpenSpec cycle:
1. **Explore** - Thought through the problem
2. **New** - Created a change container
3. **Proposal** - Captured WHY
4. **Specs** - Defined WHAT in detail
5. **Design** - Decided HOW
6. **Tasks** - Broke it into steps
7. **Apply** - Implemented the work
8. **Archive** - Preserved the record
This same rhythm works for any size change—a small fix or a major feature.
---
## Command Reference
**Core workflow:**
| Command | What it does |
|---------|--------------|
| `/opsx:propose` | Create a change and generate all artifacts |
| `/opsx:explore` | Think through problems before/during work |
| `/opsx:apply` | Implement tasks from a change |
| `/opsx:archive` | Archive a completed change |
**Additional commands:**
| Command | What it does |
|---------|--------------|
| `/opsx:new` | Start a new change, step through artifacts one at a time |
| `/opsx:continue` | Continue working on an existing change |
| `/opsx:ff` | Fast-forward: create all artifacts at once |
| `/opsx:verify` | Verify implementation matches artifacts |
---
## What's Next?
Try `/opsx:propose` on something you actually want to build. You've got the rhythm now!
```
---
## Graceful Exit Handling
### User wants to stop mid-way
If the user says they need to stop, want to pause, or seem disengaged:
```
No problem! Your change is saved at `openspec/changes/<name>/`.
To pick up where we left off later:
- `/opsx:continue <name>` - Resume artifact creation
- `/opsx:apply <name>` - Jump to implementation (if tasks exist)
The work won't be lost. Come back whenever you're ready.
```
Exit gracefully without pressure.
### User just wants command reference
If the user says they just want to see the commands or skip the tutorial:
```
## OpenSpec Quick Reference
**Core workflow:**
| Command | What it does |
|---------|--------------|
| `/opsx:propose <name>` | Create a change and generate all artifacts |
| `/opsx:explore` | Think through problems (no code changes) |
| `/opsx:apply <name>` | Implement tasks |
| `/opsx:archive <name>` | Archive when done |
**Additional commands:**
| Command | What it does |
|---------|--------------|
| `/opsx:new <name>` | Start a new change, step by step |
| `/opsx:continue <name>` | Continue an existing change |
| `/opsx:ff <name>` | Fast-forward: all artifacts at once |
| `/opsx:verify <name>` | Verify implementation |
Try `/opsx:propose` to start your first change.
```
Exit gracefully.
---
## Guardrails
- **Follow the EXPLAIN → DO → SHOW → PAUSE pattern** at key transitions (after explore, after proposal draft, after tasks, after archive)
- **Keep narration light** during implementation—teach without lecturing
- **Don't skip phases** even if the change is small—the goal is teaching the workflow
- **Pause for acknowledgment** at marked points, but don't over-pause
- **Handle exits gracefully**—never pressure the user to continue
- **Use real codebase tasks**—don't simulate or use fake examples
- **Adjust scope gently**—guide toward smaller tasks but respect user choice

View File

@@ -0,0 +1,233 @@
---
name: "OPSX: Porting Guide"
description: Generate a detailed human-readable implementation guide for porting a feature to a target codebase
category: Workflow
tags: [workflow, portability, experimental]
---
Generate a detailed human-readable porting guide for implementing an exported feature on a target codebase.
This is a **companion** to `/opsx:export-spec`. The export-spec is optimized for AI consumption (compact, precise). This guide is optimized for the **human developer** who needs to understand context, troubleshoot issues, and make judgment calls when the AI agent on the target hits problems.
---
**Input**: The argument after `/opsx:porting-guide` is a change name or feature name that has already been exported via `/opsx:export-spec`. If omitted, prompt for selection.
**Prerequisites**: An export-spec must already exist in `openspec/exports/`. This skill reads the export-spec to stay consistent with it.
**Steps**
1. **Locate the export artifacts**
a. Check `openspec/exports/` for matching files:
- `<name>-spec.md` (portable spec — required)
- `<name>-openspec.md` (OpenSpec bundle — optional)
b. If not found, prompt with **AskUserQuestion tool**:
> "No export-spec found for '<name>'. Available exports: [list]. Which one?"
c. Read the export-spec fully — the porting guide must not contradict it.
2. **Read the source material**
Same sources as export-spec, but read for **context and rationale** rather than contracts:
a. All archived change proposals, designs, and tasks in `openspec/changes/archive/`
b. All main specs in `openspec/specs/`
c. The actual implementation source files
d. Any existing export-spec and OpenSpec bundle
Focus on extracting:
- **Decision rationale**: WHY each design choice was made
- **Rejected alternatives**: what was considered and discarded
- **Friction points**: where the source implementation hit problems
- **Implicit knowledge**: things the source developer knew but didn't document
- **Platform traps**: runtime behaviors that aren't obvious from the code
3. **Gather target context**
If not already captured in the export-spec, use **AskUserQuestion tool** to learn:
> "To write accurate porting notes, I need to understand the target:
> 1. What patterns does the target use for DI registration? (manual, Scrutor, Autofac?)
> 2. What's the deployment model? (Azure, on-prem, Docker?)
> 3. Are there known constraints? (network restrictions, NuGet source limits, proxy requirements?)
> 4. Who will be implementing — developer familiarity with the source stack?"
This shapes the level of explanation and which friction points to highlight.
4. **Generate the porting guide**
Create a comprehensive markdown document structured for a human reader.
Use narrative prose where it aids understanding, tables for quick reference,
and checklists for actionable steps.
### Structure:
```markdown
# Porting Guide: <Feature> → <Target>
## Source: <source project> | Export: <export-spec filename>
## How to Use This Guide
Brief orientation:
- Relationship to the AI-targeted export-spec
- When to read this vs when to let the AI work
- How sections are organized
## Architecture Overview
2-3 paragraphs a human can read in 5 minutes to understand the full feature.
Written as narrative, not bullet points. Cover:
- What the feature does end-to-end (user perspective)
- How data flows through the system (request → processing → response)
- What external dependencies exist and why
- The "one thing you must understand" about this feature
## Design Decisions (Detailed)
For EACH significant design decision:
### <Decision Title>
**What we chose:** <the approach>
**Why:** <rationale — the real reason, not the obvious one>
**What we rejected:**
- <Alternative A> — rejected because <specific reason>
- <Alternative B> — rejected because <specific reason>
**When you'd revisit this:** <conditions under which this decision should change>
**Target adaptation:** <how this maps to the target's patterns, what might need adjusting>
Decisions to always cover:
- Framework/library choices (SK over raw HTTP, SSE over WebSocket, etc.)
- Lifetime/scope decisions (singleton vs scoped vs transient)
- State management approach
- Streaming strategy
- Security decisions (sanitization, auth, CORS)
- File/folder organization
## Source → Target Mapping
Side-by-side mapping table showing how source concepts translate to target:
| Source (ChatAgent) | Target (CRC) | Notes |
|---|---|---|
| `ChatAgent.Api/` | `CRC.Server/` | CRC uses hosted model, not standalone API |
| `ChatAgent.Client/` | `CRC.Client/` | Same WASM pattern |
| `builder.Services.AddScoped<T>()` | Check if Scrutor handles this | CRC may auto-scan |
| ... | ... | ... |
Flag EVERY known divergence, no matter how small. The small ones cause the most debugging time.
## Task-by-Task Implementation Notes
For EACH task in the OpenSpec tasks.md:
### <Task ID>: <Task Title>
**Prerequisites:** What must be completed first and verified working.
**Context:** Why this task exists and what it accomplishes in the bigger picture.
A developer who understands the "why" makes better judgment calls when adapting.
**Step-by-step:**
1. <Step with enough detail that someone unfamiliar with the source can follow>
2. <Step>
3. <Step>
**Expected friction on target:**
- <Specific thing that will likely need adaptation and why>
- <CRC pattern that differs from the source approach>
**Verify it works:**
- <Concrete test: "Navigate to X, click Y, you should see Z">
- <Log check: "Watch for 'SK: Auto-invoking function' in Serilog output">
- <Build check: "dotnet build should produce 0 warnings related to this task">
**If it breaks — diagnostic checklist:**
- Symptom: <what you'll see>
Cause: <most likely reason>
Fix: <specific remediation>
- Symptom: <what you'll see>
Cause: <most likely reason>
Fix: <specific remediation>
## Troubleshooting Reference
Comprehensive symptom → cause → fix table for known porting issues.
Organized by category (network, DI, UI, streaming, auth, config).
| Symptom | Likely Cause | Fix |
|---|---|---|
| 404 on /v1/chat/completions | Base URL missing `/v1` | Add `/v1` to `NlxvaPricer:LlmBaseUrl` |
| CORS 403 on SSE endpoint | CORS policy doesn't cover `text/event-stream` | Add origin to CORS policy |
| Streaming hangs, no tokens | `SetBrowserResponseStreamingEnabled` missing | Add to HttpRequestMessage |
| `EndOfStream` throws | Synchronous peek on async-only WASM stream | Use `ReadLineAsync() != null` loop |
| Markdown not rendering | MarkdownService not registered in DI | Add `AddSingleton<MarkdownService>()` |
| Tools never called by LLM | Plugin not imported on request | Move ImportPluginFromObject into action method |
| ... | ... | ... |
Include at least 10-15 entries covering the most likely failure modes.
## Configuration Checklist
Every config key the feature needs, organized as a checklist:
- [ ] `NlxvaPricer:LlmBaseUrl` — Where: `appsettings.json` (CRC.Server) — Default: `http://localhost:8317/v1` — What happens if missing: Falls back to default, fails if proxy not running
- [ ] `NlxvaPricer:LlmModel` — Where: `appsettings.json` — Default: `claude-sonnet-4-6`
- [ ] ... (every key)
## Dependency & Package Notes
For each new package:
- Package name and version constraint
- Why it's needed (one sentence)
- .NET version compatibility notes
- Known conflicts with packages the target might already have
- NuGet source: public nuget.org or internal feed?
(CRC uses internal GV Artifactory — flag if a package might not be available there)
## Rollback Plan
If the feature needs to be removed:
- Which files were added (safe to delete)
- Which files were modified and what was added (revert specific sections)
- Which NuGet packages were added (remove from csproj)
- Which config keys were added (remove from appsettings.json)
```
5. **Cross-check against export-spec**
Before writing, verify:
- Every contract in the export-spec is explained in the porting guide
- Every critical pattern has a troubleshooting entry
- No contradictions between the two documents
- The porting guide adds CONTEXT, not ALTERNATIVE instructions
6. **Write the output**
Save to: `openspec/exports/<name>-porting-guide.md`
Display a summary of sections generated and key highlights.
**Guardrails**
- **Narrative over telegraphic**: This is for humans. Use complete sentences. Explain the "why."
- **Never contradict the export-spec**: The AI spec is the source of truth for contracts and patterns. The porting guide explains and contextualizes, it doesn't override.
- **Anticipate the debugging session**: For every task, imagine the developer hitting a wall. What would they need to know? Write that down.
- **Be specific about the target**: Generic advice ("check your config") is useless. Specific advice ("CRC uses `gv_web_config.csv` for primary config but `appsettings.json` for secondary — LLM config goes in appsettings") saves hours.
- **Include the embarrassing details**: The things that took 30 minutes to figure out in the source (like the `/v1` URL requirement, or `EndOfStream` hanging) — those are the most valuable entries.
- **Flag NuGet source issues**: If the target uses an internal feed (like CRC's GV Artifactory with nuget.org disabled), flag every package that needs to come from a specific source.
- **No code blocks for code's sake**: The export-spec has the exact code. The porting guide references it ("see Critical Pattern #2 in the export-spec") rather than duplicating it. Only include code when it clarifies a specific porting concern.
- **Version-stamp the guide**: Include the date and source commit hash so the reader knows what state the guide reflects.
**Anti-patterns**
1. **Don't duplicate the export-spec.** The export-spec has exact contracts and code snippets. The porting guide has context, rationale, and troubleshooting. They complement each other.
2. **Don't be abstract.** "There might be DI issues" is useless. "CRC uses Scrutor for assembly scanning. If Scrutor auto-registers ExtractionPlugin before your manual AddScoped<ExtractionPlugin>(), you'll get a duplicate registration. Check with `builder.Services.Where(s => s.ServiceType == typeof(ExtractionPlugin))` in a breakpoint" is useful.
3. **Don't skip the obvious.** The developer may be senior but unfamiliar with Semantic Kernel. Explain SK concepts (Kernel, plugins, FunctionChoiceBehavior) in plain language.
4. **Don't assume the happy path.** Network restrictions, proxy requirements, package feed limitations, and auth quirks are the norm in enterprise environments. Address them.
ARGUMENTS: $ARGUMENTS

View File

@@ -0,0 +1,134 @@
---
name: "OPSX: Sync"
description: Sync delta specs from a change to main specs
category: Workflow
tags: [workflow, specs, experimental]
---
Sync delta specs from a change to main specs.
This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
**Input**: Optionally specify a change name after `/opsx:sync` (e.g., `/opsx:sync add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show changes that have delta specs (under `specs/` directory).
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Find delta specs**
Look for delta spec files in `openspec/changes/<name>/specs/*/spec.md`.
Each delta spec file contains sections like:
- `## ADDED Requirements` - New requirements to add
- `## MODIFIED Requirements` - Changes to existing requirements
- `## REMOVED Requirements` - Requirements to remove
- `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
If no delta specs found, inform user and stop.
3. **For each delta spec, apply changes to main specs**
For each capability with a delta spec at `openspec/changes/<name>/specs/<capability>/spec.md`:
a. **Read the delta spec** to understand the intended changes
b. **Read the main spec** at `openspec/specs/<capability>/spec.md` (may not exist yet)
c. **Apply changes intelligently**:
**ADDED Requirements:**
- If requirement doesn't exist in main spec → add it
- If requirement already exists → update it to match (treat as implicit MODIFIED)
**MODIFIED Requirements:**
- Find the requirement in main spec
- Apply the changes - this can be:
- Adding new scenarios (don't need to copy existing ones)
- Modifying existing scenarios
- Changing the requirement description
- Preserve scenarios/content not mentioned in the delta
**REMOVED Requirements:**
- Remove the entire requirement block from main spec
**RENAMED Requirements:**
- Find the FROM requirement, rename to TO
d. **Create new main spec** if capability doesn't exist yet:
- Create `openspec/specs/<capability>/spec.md`
- Add Purpose section (can be brief, mark as TBD)
- Add Requirements section with the ADDED requirements
4. **Show summary**
After applying all changes, summarize:
- Which capabilities were updated
- What changes were made (requirements added/modified/removed/renamed)
**Delta Spec Format Reference**
```markdown
## ADDED Requirements
### Requirement: New Feature
The system SHALL do something new.
#### Scenario: Basic case
- **WHEN** user does X
- **THEN** system does Y
## MODIFIED Requirements
### Requirement: Existing Feature
#### Scenario: New scenario to add
- **WHEN** user does A
- **THEN** system does B
## REMOVED Requirements
### Requirement: Deprecated Feature
## RENAMED Requirements
- FROM: `### Requirement: Old Name`
- TO: `### Requirement: New Name`
```
**Key Principle: Intelligent Merging**
Unlike programmatic merging, you can apply **partial updates**:
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
- The delta represents *intent*, not a wholesale replacement
- Use your judgment to merge changes sensibly
**Output On Success**
```
## Specs Synced: <change-name>
Updated main specs:
**<capability-1>**:
- Added requirement: "New Feature"
- Modified requirement: "Existing Feature" (added 1 scenario)
**<capability-2>**:
- Created new spec file
- Added requirement: "Another Feature"
Main specs are now updated. The change remains active - archive when implementation is complete.
```
**Guardrails**
- Read both delta and main specs before making changes
- Preserve existing content not mentioned in delta
- If something is unclear, ask for clarification
- Show what you're changing as you go
- The operation should be idempotent - running twice should give same result

View File

@@ -0,0 +1,164 @@
---
name: "OPSX: Verify"
description: Verify implementation matches change artifacts before archiving
category: Workflow
tags: [workflow, verify, experimental]
---
Verify that an implementation matches the change artifacts (specs, tasks, design).
**Input**: Optionally specify a change name after `/opsx:verify` (e.g., `/opsx:verify add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show changes that have implementation tasks (tasks artifact exists).
Include the schema used for each change if available.
Mark changes with incomplete tasks as "(In Progress)".
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- Which artifacts exist for this change
3. **Get the change directory and load artifacts**
```bash
openspec instructions apply --change "<name>" --json
```
This returns the change directory and context files. Read all available artifacts from `contextFiles`.
4. **Initialize verification report structure**
Create a report structure with three dimensions:
- **Completeness**: Track tasks and spec coverage
- **Correctness**: Track requirement implementation and scenario coverage
- **Coherence**: Track design adherence and pattern consistency
Each dimension can have CRITICAL, WARNING, or SUGGESTION issues.
5. **Verify Completeness**
**Task Completion**:
- If tasks.md exists in contextFiles, read it
- Parse checkboxes: `- [ ]` (incomplete) vs `- [x]` (complete)
- Count complete vs total tasks
- If incomplete tasks exist:
- Add CRITICAL issue for each incomplete task
- Recommendation: "Complete task: <description>" or "Mark as done if already implemented"
**Spec Coverage**:
- If delta specs exist in `openspec/changes/<name>/specs/`:
- Extract all requirements (marked with "### Requirement:")
- For each requirement:
- Search codebase for keywords related to the requirement
- Assess if implementation likely exists
- If requirements appear unimplemented:
- Add CRITICAL issue: "Requirement not found: <requirement name>"
- Recommendation: "Implement requirement X: <description>"
6. **Verify Correctness**
**Requirement Implementation Mapping**:
- For each requirement from delta specs:
- Search codebase for implementation evidence
- If found, note file paths and line ranges
- Assess if implementation matches requirement intent
- If divergence detected:
- Add WARNING: "Implementation may diverge from spec: <details>"
- Recommendation: "Review <file>:<lines> against requirement X"
**Scenario Coverage**:
- For each scenario in delta specs (marked with "#### Scenario:"):
- Check if conditions are handled in code
- Check if tests exist covering the scenario
- If scenario appears uncovered:
- Add WARNING: "Scenario not covered: <scenario name>"
- Recommendation: "Add test or implementation for scenario: <description>"
7. **Verify Coherence**
**Design Adherence**:
- If design.md exists in contextFiles:
- Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:")
- Verify implementation follows those decisions
- If contradiction detected:
- Add WARNING: "Design decision not followed: <decision>"
- Recommendation: "Update implementation or revise design.md to match reality"
- If no design.md: Skip design adherence check, note "No design.md to verify against"
**Code Pattern Consistency**:
- Review new code for consistency with project patterns
- Check file naming, directory structure, coding style
- If significant deviations found:
- Add SUGGESTION: "Code pattern deviation: <details>"
- Recommendation: "Consider following project pattern: <example>"
8. **Generate Verification Report**
**Summary Scorecard**:
```
## Verification Report: <change-name>
### Summary
| Dimension | Status |
|--------------|------------------|
| Completeness | X/Y tasks, N reqs|
| Correctness | M/N reqs covered |
| Coherence | Followed/Issues |
```
**Issues by Priority**:
1. **CRITICAL** (Must fix before archive):
- Incomplete tasks
- Missing requirement implementations
- Each with specific, actionable recommendation
2. **WARNING** (Should fix):
- Spec/design divergences
- Missing scenario coverage
- Each with specific recommendation
3. **SUGGESTION** (Nice to fix):
- Pattern inconsistencies
- Minor improvements
- Each with specific recommendation
**Final Assessment**:
- If CRITICAL issues: "X critical issue(s) found. Fix before archiving."
- If only warnings: "No critical issues. Y warning(s) to consider. Ready for archive (with noted improvements)."
- If all clear: "All checks passed. Ready for archive."
**Verification Heuristics**
- **Completeness**: Focus on objective checklist items (checkboxes, requirements list)
- **Correctness**: Use keyword search, file path analysis, reasonable inference - don't require perfect certainty
- **Coherence**: Look for glaring inconsistencies, don't nitpick style
- **False Positives**: When uncertain, prefer SUGGESTION over WARNING, WARNING over CRITICAL
- **Actionability**: Every issue must have a specific recommendation with file/line references where applicable
**Graceful Degradation**
- If only tasks.md exists: verify task completion only, skip spec/design checks
- If tasks + specs exist: verify completeness and correctness, skip design
- If full artifacts: verify all three dimensions
- Always note which checks were skipped and why
**Output Format**
Use clear markdown with:
- Table for summary scorecard
- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION)
- Code references in format: `file.ts:123`
- Specific, actionable recommendations
- No vague suggestions like "consider reviewing"

View File

@@ -0,0 +1,246 @@
---
name: openspec-bulk-archive-change
description: Archive multiple completed changes at once. Use when archiving several parallel changes.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Archive multiple completed changes in a single operation.
This skill allows you to batch-archive changes, handling spec conflicts intelligently by checking the codebase to determine what's actually implemented.
**Input**: None required (prompts for selection)
**Steps**
1. **Get active changes**
Run `openspec list --json` to get all active changes.
If no active changes exist, inform user and stop.
2. **Prompt for change selection**
Use **AskUserQuestion tool** with multi-select to let user choose changes:
- Show each change with its schema
- Include an option for "All changes"
- Allow any number of selections (1+ works, 2+ is the typical use case)
**IMPORTANT**: Do NOT auto-select. Always let the user choose.
3. **Batch validation - gather status for all selected changes**
For each selected change, collect:
a. **Artifact status** - Run `openspec status --change "<name>" --json`
- Parse `schemaName` and `artifacts` list
- Note which artifacts are `done` vs other states
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
- If no tasks file exists, note as "No tasks"
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
- List which capability specs exist
- For each, extract requirement names (lines matching `### Requirement: <name>`)
4. **Detect spec conflicts**
Build a map of `capability -> [changes that touch it]`:
```
auth -> [change-a, change-b] <- CONFLICT (2+ changes)
api -> [change-c] <- OK (only 1 change)
```
A conflict exists when 2+ selected changes have delta specs for the same capability.
5. **Resolve conflicts agentically**
**For each conflict**, investigate the codebase:
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
b. **Search the codebase** for implementation evidence:
- Look for code implementing requirements from each delta spec
- Check for related files, functions, or tests
c. **Determine resolution**:
- If only one change is actually implemented -> sync that one's specs
- If both implemented -> apply in chronological order (older first, newer overwrites)
- If neither implemented -> skip spec sync, warn user
d. **Record resolution** for each conflict:
- Which change's specs to apply
- In what order (if both)
- Rationale (what was found in codebase)
6. **Show consolidated status table**
Display a table summarizing all changes:
```
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|---------------------|-----------|-------|---------|-----------|--------|
| schema-management | Done | 5/5 | 2 delta | None | Ready |
| project-config | Done | 3/3 | 1 delta | None | Ready |
| add-oauth | Done | 4/4 | 1 delta | auth (!) | Ready* |
| add-verify-skill | 1 left | 2/5 | None | None | Warn |
```
For conflicts, show the resolution:
```
* Conflict resolution:
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
```
For incomplete changes, show warnings:
```
Warnings:
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
```
7. **Confirm batch operation**
Use **AskUserQuestion tool** with a single confirmation:
- "Archive N changes?" with options based on status
- Options might include:
- "Archive all N changes"
- "Archive only N ready changes (skip incomplete)"
- "Cancel"
If there are incomplete changes, make clear they'll be archived with warnings.
8. **Execute archive for each confirmed change**
Process changes in the determined order (respecting conflict resolution):
a. **Sync specs** if delta specs exist:
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
- For conflicts, apply in resolved order
- Track if sync was done
b. **Perform the archive**:
```bash
mkdir -p openspec/changes/archive
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
```
c. **Track outcome** for each change:
- Success: archived successfully
- Failed: error during archive (record error)
- Skipped: user chose not to archive (if applicable)
9. **Display summary**
Show final results:
```
## Bulk Archive Complete
Archived 3 changes:
- schema-management-cli -> archive/2026-01-19-schema-management-cli/
- project-config -> archive/2026-01-19-project-config/
- add-oauth -> archive/2026-01-19-add-oauth/
Skipped 1 change:
- add-verify-skill (user chose not to archive incomplete)
Spec sync summary:
- 4 delta specs synced to main specs
- 1 conflict resolved (auth: applied both in chronological order)
```
If any failures:
```
Failed 1 change:
- some-change: Archive directory already exists
```
**Conflict Resolution Examples**
Example 1: Only one implemented
```
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
Checking add-oauth:
- Delta adds "OAuth Provider Integration" requirement
- Searching codebase... found src/auth/oauth.ts implementing OAuth flow
Checking add-jwt:
- Delta adds "JWT Token Handling" requirement
- Searching codebase... no JWT implementation found
Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
```
Example 2: Both implemented
```
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
Checking add-rest-api (created 2026-01-10):
- Delta adds "REST Endpoints" requirement
- Searching codebase... found src/api/rest.ts
Checking add-graphql (created 2026-01-15):
- Delta adds "GraphQL Schema" requirement
- Searching codebase... found src/api/graphql.ts
Resolution: Both implemented. Will apply add-rest-api specs first,
then add-graphql specs (chronological order, newer takes precedence).
```
**Output On Success**
```
## Bulk Archive Complete
Archived N changes:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
- <change-2> -> archive/YYYY-MM-DD-<change-2>/
Spec sync summary:
- N delta specs synced to main specs
- No conflicts (or: M conflicts resolved)
```
**Output On Partial Success**
```
## Bulk Archive Complete (partial)
Archived N changes:
- <change-1> -> archive/YYYY-MM-DD-<change-1>/
Skipped M changes:
- <change-2> (user chose not to archive incomplete)
Failed K changes:
- <change-3>: Archive directory already exists
```
**Output When No Changes**
```
## No Changes to Archive
No active changes found. Create a new change to get started.
```
**Guardrails**
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
- Always prompt for selection, never auto-select
- Detect spec conflicts early and resolve by checking codebase
- When both changes are implemented, apply specs in chronological order
- Skip spec sync only when implementation is missing (warn user)
- Show clear per-change status before confirming
- Use single confirmation for entire batch
- Track and report all outcomes (success/skip/fail)
- Preserve .openspec.yaml when moving to archive
- Archive directory target uses current date: YYYY-MM-DD-<name>
- If archive target exists, fail that change but continue with others

View File

@@ -0,0 +1,118 @@
---
name: openspec-continue-change
description: Continue working on an OpenSpec change by creating the next artifact. Use when the user wants to progress their change, create the next artifact, or continue their workflow.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Continue working on a change by creating the next artifact.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes sorted by most recently modified. Then use the **AskUserQuestion tool** to let the user select which change to work on.
Present the top 3-4 most recently modified changes as options, showing:
- Change name
- Schema (from `schema` field if present, otherwise "spec-driven")
- Status (e.g., "0/5 tasks", "complete", "no tasks")
- How recently it was modified (from `lastModified` field)
Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to continue.
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check current status**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand current state. The response includes:
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
- `isComplete`: Boolean indicating if all artifacts are complete
3. **Act based on status**:
---
**If all artifacts are complete (`isComplete: true`)**:
- Congratulate the user
- Show final status including the schema used
- Suggest: "All artifacts created! You can now implement this change or archive it."
- STOP
---
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
- Pick the FIRST artifact with `status: "ready"` from the status output
- Get its instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- Parse the JSON. The key fields are:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- **Create the artifact file**:
- Read any completed dependency files for context
- Use `template` as the structure - fill in its sections
- Apply `context` and `rules` as constraints when writing - but do NOT copy them into the file
- Write to the output path specified in instructions
- Show what was created and what's now unlocked
- STOP after creating ONE artifact
---
**If no artifacts are ready (all blocked)**:
- This shouldn't happen with a valid schema
- Show status and suggest checking for issues
4. **After creating an artifact, show progress**
```bash
openspec status --change "<name>"
```
**Output**
After each invocation, show:
- Which artifact was created
- Schema workflow being used
- Current progress (N/M complete)
- What artifacts are now unlocked
- Prompt: "Want to continue? Just ask me to continue or tell me what to do next."
**Artifact Creation Guidelines**
The artifact types and their purpose depend on the schema. Use the `instruction` field from the instructions output to understand what to create.
Common artifact patterns:
**spec-driven schema** (proposal → specs → design → tasks):
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
- The Capabilities section is critical - each capability listed will need a spec file.
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
- **design.md**: Document technical decisions, architecture, and implementation approach.
- **tasks.md**: Break down implementation into checkboxed tasks.
For other schemas, follow the `instruction` field from the CLI output.
**Guardrails**
- Create ONE artifact per invocation
- Always read dependency artifacts before creating a new one
- Never skip artifacts or create out of order
- If context is unclear, ask the user before creating
- Verify the artifact file exists after writing before marking progress
- Use the schema's artifact sequence, don't assume specific artifact names
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output

View File

@@ -0,0 +1,217 @@
---
name: openspec-export-spec
description: Export a feature as a compact, portable spec that an AI agent (Copilot, Claude) on a sandboxed machine can implement from. Optimized for minimal hand-typing.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Export a feature as a portable spec for AI-assisted reimplementation on a sandboxed machine.
Instead of retyping code, you retype a compact spec. The AI on the sandbox (Copilot, Claude, OpenSpec) generates the code from the spec.
---
**Input**: A change name (active or archived), or a description of the feature. If omitted, prompt for selection.
**Steps**
1. **Identify the source feature**
Same as `openspec-extract-feature` step 1:
- Check active changes and archive for the change name
- If not found, prompt with **AskUserQuestion tool**
- Read all artifacts: `proposal.md`, `design.md`, `tasks.md`, specs
2. **Analyze dependency chain (cumulative mode)**
Features often build on each other. Before generating the spec, determine
what the target feature depends on.
a. **Read all archived change proposals** in `openspec/changes/archive/` (sorted by date).
For each, read `proposal.md` to understand what it adds and what it depends on.
b. **Build a dependency graph**:
- Which changes does the target feature require?
- Which earlier changes are superseded? (e.g., if Change 6 replaces Change 3's
implementation, include Change 6's version, not Change 3's)
- Which changes are unrelated and can be skipped?
c. **Ask the user** using **AskUserQuestion tool**:
> "This feature depends on earlier changes. How should I scope the spec?"
>
> Options:
> 1. **Cumulative** — include all foundation this feature needs (recommended if target is a fresh codebase)
> 2. **Delta only** — just what this specific change adds (use if target already has the foundation)
> 3. **Custom** — let me pick which dependencies to include
Show the dependency chain so the user can decide.
d. **In cumulative mode**: The spec covers the entire stack from foundation to feature.
- Merge packages, components, wiring from all included changes
- Skip superseded components (use the latest version)
- The spec should read as a single coherent feature, not a list of changes
e. **In delta mode**: The spec covers only the selected change.
- Add a "Assumes" section listing what must already exist in the target
3. **Read the actual implementation**
Read all source files that were created or modified by this feature
(and its dependencies if cumulative).
The spec must reflect what was actually built, not just what was planned.
4. **Determine the target context**
Use **AskUserQuestion tool** to ask:
> "Tell me about the target codebase:
> 1. Project name / root namespace
> 2. Existing stack (ASP.NET Core? Blazor? MudBlazor?)
> 3. Does it already have any of these? (controllers, DI setup, chat endpoint)
> 4. Does the target have OpenSpec? GitHub Copilot? Claude Code?"
This shapes what the spec assumes vs what it must specify.
5. **Generate the portable spec**
Create a single markdown document that is:
- **Compact**: Target ~30-50 lines for a medium feature
- **Precise**: Unambiguous enough for an AI to implement correctly
- **Self-contained**: No references to external files or repos
- **Stack-aware**: Uses the right terminology for the target stack
Structure:
```markdown
# Feature: <Name>
## Target: <project name> (<stack>)
## Packages
<list with versions>
## Architecture
<2-3 sentence overview of how the pieces fit together>
## Components
### <Component 1>: <path hint>
- <What it does — 1 line>
- <Key behavior — 1 line>
- <Interface/contract — 1 line>
### <Component 2>: <path hint>
...
## Contracts
<API shapes, model definitions, SSE formats — the things that MUST be exact>
## Wiring
<DI registration, middleware order, configuration keys — in dependency order>
## Behavior
<Key behavioral requirements that aren't obvious from the structure>
```
**Compression strategies:**
- Use bullet points, not prose
- Specify contracts precisely (field names, types, API shapes)
- Let the AI infer standard patterns (error handling, null checks, etc.)
- Only specify non-obvious behavior (the surprising parts)
- Omit anything the AI would do by default for the given stack
6. **Estimate typing effort**
Count characters in the spec. Compare to the code recipe equivalent.
Show the compression ratio:
```
Code recipe: ~120 lines to type
This spec: ~35 lines to type
Compression: 3.4x
```
7. **Optionally generate an OpenSpec-compatible version**
If the target has OpenSpec, also generate a version structured as:
- A `proposal.md` (minimal — 5-10 lines)
- A `tasks.md` (implementation steps the target AI follows)
These can be even more compact because OpenSpec provides the scaffolding.
Save this variant alongside the main spec.
8. **Write the output**
Save to: `openspec/exports/<change-name>-spec.md`
If OpenSpec variant: `openspec/exports/<change-name>-openspec.md`
Display the full content for review.
**Output Format**
```markdown
# Feature: Semantic Kernel Chat with Tool Calling
## Target: ApplicationX (ASP.NET Core + Blazor WASM + MudBlazor)
## Packages
- Microsoft.SemanticKernel 1.74.0
- Microsoft.SemanticKernel.Connectors.OpenAI 1.74.0
## Architecture
POST /api/chat endpoint accepts messages, runs them through SK's chat completion
with auto tool calling enabled, streams response as SSE. An ExtractionPlugin
validates structured data extracted by the LLM.
## Components
### ChatController: Controllers/ChatController.cs
- POST endpoint, injects Kernel via DI
- Converts ChatMessage[] to SK ChatHistory
- Streams via GetStreamingChatMessageContentsAsync
- Outputs SSE: `data: {"text":"..."}\n\n` then `data: [DONE]\n\n`
### ExtractionPlugin: Plugins/ExtractionPlugin.cs
- [KernelFunction("validate_extracted_fields")]
- Accepts JSON string, deserializes to ExtractedFields
- Returns {"isValid": bool, "errors": string[]}
### ExtractedFields: Models/ExtractedFields.cs
- Required: Client(string), Project(string), Hours(decimal), Rate(decimal), Currency(string), Date(string)
- Optional: Description(string), PoNumber(string)
### ValidationResult: Models/ValidationResult.cs
- IsValid(bool), Errors(List<string>)
### ChatRequest/ChatMessage: Shared/Models/
- ChatRequest: Messages(List<ChatMessage>)
- ChatMessage: Role(string), Content(string)
## Wiring (Program.cs, add after AddControllers)
- AddOpenAIChatCompletion(model, endpoint with /v1 suffix, apiKey)
- AddKernel()
- AddSingleton<ExtractionPlugin>()
- UseCors for Blazor client origin
## Behavior
- FunctionChoiceBehavior.Auto() enables autonomous tool calling
- SK's built-in limit prevents runaway tool call loops
- Plugin imported per-request via _kernel.ImportPluginFromObject
- Base URL must include /v1 — OpenAI SDK appends chat/completions directly
```
**Lines to type: ~35 | Code equivalent: ~150 lines | Compression: 4.3x**
**Guardrails**
- Prioritize precision over brevity — an ambiguous spec wastes more time than a slightly longer one
- Always include exact field names, types, and API shapes — these are the hardest to guess
- Include non-obvious gotchas (like the /v1 base URL requirement)
- Test the spec mentally: could an AI implement this correctly without seeing the original code?
- If the feature is too complex for a single spec page (~50+ lines), split into multiple specs by component
- Always show the compression ratio so the user can decide between spec and code recipe
- The spec must be readable when printed in monospace — no wide tables or long lines
- In cumulative mode, the spec must read as one coherent feature — not a list of sequential changes
- Skip superseded components — always describe the latest version of each piece
- In delta mode, add an "Assumes" section so the target AI knows what must already exist
- In the output header, note which changes were included and which were skipped

View File

@@ -0,0 +1,170 @@
---
name: openspec-extract-feature
description: Extract a feature from a change (archived or active) into a minimal, printable code recipe optimized for manual retyping into a sandboxed codebase.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Extract a feature into a minimal, printable code recipe for manual reimplementation.
Generates a markdown document with:
- Package dependencies
- Ordered code blocks (no comments, no boilerplate)
- Clear markers for generic vs domain-specific code
Print it, take it to the sandbox, type it in.
---
**Input**: A change name (active or archived), a git commit range, or a list of files. If omitted, prompt for selection.
**Steps**
1. **Identify the source feature**
If a change name is provided:
- Check active changes: `openspec list --json`
- Check archive: look in `openspec/changes/archive/` for directories ending with the name
- If found, read its artifacts: `proposal.md`, `design.md`, `tasks.md`
If no name provided:
- Run `openspec list --json` and list archived changes
- Use **AskUserQuestion tool** to let the user select
If a file list or commit range is provided instead:
- Read those files directly
- Identify the feature from the code
2. **Analyze dependency chain (cumulative mode)**
Features are often not independent — they build on each other. Before generating
the recipe, determine what the target feature depends on.
a. **Read all archived change proposals** in `openspec/changes/archive/` (sorted by date).
For each, read `proposal.md` to understand what it adds and what it depends on.
b. **Build a dependency graph**:
- Which changes does the target feature require?
- Which earlier changes are superseded? (e.g., if Change 6 replaces Change 3's
implementation, include Change 6's version, not Change 3's)
- Which changes are unrelated and can be skipped?
c. **Ask the user** using **AskUserQuestion tool**:
> "This feature depends on earlier changes. How should I scope the recipe?"
>
> Options:
> 1. **Cumulative** — include all foundation code this feature needs (recommended if target is a fresh codebase)
> 2. **Delta only** — just the code this specific change adds (use if target already has the foundation)
> 3. **Custom** — let me pick which dependencies to include
Show the dependency chain so the user can decide, e.g.:
```
migrate-to-semantic-kernel depends on:
← basic-chat-interface (UI + shared models)
← wire-responses-api (superseded — SK replaces the proxy)
← multi-turn-conversations (chat history)
```
d. **In cumulative mode**: Merge the dependency chain into a single coherent recipe.
- Walk changes in dependency order
- Skip superseded code (use the latest version of each file)
- Collapse packages from all changes into one prerequisites section
- If a file was created in Change 2 and modified in Change 5, include only the final version
e. **In delta mode**: Include only the code from the selected change.
3. **Analyze the feature scope**
From the change artifacts and/or code, determine:
- Which files were created or modified (across the full dependency chain if cumulative)
- What NuGet/npm packages were added
- What the dependency order is (e.g., models before controllers)
- What is generic infrastructure vs domain-specific logic
Read all relevant source files in the current codebase to get the actual current code.
**Always read the final current version of each file**, not intermediate versions.
3. **Generate the code recipe**
Create a markdown document with these sections, in this order:
**Header**: Feature name, source change, date
**Prerequisites**: Package references with exact versions
**Steps**: Ordered by dependency. Each step contains:
- Step number and action (e.g., "New file", "Add to existing file", "Modify")
- Target path (relative, adaptable)
- Namespace placeholder: `__YOUR_NAMESPACE__` where the target project namespace goes
- **Code block**: The actual code, stripped of:
- All comments (// and /* */ and /// XML docs)
- Redundant blank lines
- Verbose variable names that can be shortened
- Any code not directly related to the feature
- If the step modifies an existing file: show only the code to add, with a brief marker for insertion point (e.g., "Add after AddControllers()")
**Domain-Specific Sections**: Clearly marked with `// ADAPT:` prefix explaining what to change for the target domain
4. **Optimize for retyping**
Review the generated document and:
- Merge small files if they can be combined
- Remove any using statements that the IDE will auto-add
- Shorten any unnecessarily verbose code
- Ensure no step exceeds ~40 lines (split if needed)
- Add line counts per step so the user can estimate effort
- Total the overall line count at the top
5. **Write the output**
Save to: `openspec/exports/<change-name>-recipe.md`
Also display the full content so the user can review it immediately.
**Output Format**
```markdown
# Feature Recipe: <Feature Name>
**Source**: <change-name> | **Lines to type**: ~<N>
## Prerequisites
- `Microsoft.SemanticKernel` 1.74.0
- `Microsoft.SemanticKernel.Connectors.OpenAI` 1.74.0
## Step 1: <Action> — <path> (~N lines)
```csharp
<stripped code>
```
## Step 2: <Action> — <path> (~N lines)
Insert after `<marker>`:
```csharp
<stripped code>
```
## Domain-Specific (adapt these)
### ExtractionPlugin.cs
```csharp
// ADAPT: Replace field names and validation logic for your domain
<code>
```
```
**Guardrails**
- Never include comments in code blocks — the goal is minimum keystrokes
- Always read the actual current source files, not just the change artifacts
- Preserve compilation order: models → services → controllers → DI registration
- Mark domain-specific code clearly so the user knows what to adapt vs copy verbatim
- Keep each step self-contained — the user may take breaks between steps
- If a feature spans more than ~200 lines of stripped code, warn the user and suggest using `/opsx:export-spec` instead
- Output must be valid markdown that renders well when printed
- When in cumulative mode, skip superseded code — always use the latest version of each file
- In the output header, show which changes were included and which were skipped (with reason)
- If a dependency chain is long (4+ changes), suggest `/opsx:export-spec` as a more efficient alternative

View File

@@ -0,0 +1,101 @@
---
name: openspec-ff-change
description: Fast-forward through OpenSpec artifact creation. Use when the user wants to quickly create all artifacts needed for implementation without stepping through each one individually.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Fast-forward through artifact creation - generate everything needed to start implementation in one go.
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
**Steps**
1. **If no clear input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
```bash
openspec new change "<name>"
```
This creates a scaffolded change at `openspec/changes/<name>/`.
3. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts with their status and dependencies
4. **Create artifacts in sequence until apply-ready**
Use the **TodoWrite tool** to track progress through the artifacts.
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "✓ Created <artifact-id>"
b. **Continue until all `applyRequires` artifacts are complete**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
5. **Show final status**
```bash
openspec status --change "<name>"
```
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
**Artifact Creation Guidelines**
- Follow the `instruction` field from `openspec instructions` for each artifact type
- The schema defines what each artifact should contain - follow it
- Read dependency artifacts for context before creating new ones
- Use `template` as the structure for your output file - fill in its sections
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
- These guide what you write, but should never appear in the output
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
- Always read dependency artifacts before creating a new one
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
- If a change with that name already exists, suggest continuing that change instead
- Verify each artifact file exists after writing before proceeding to next

View File

@@ -0,0 +1,74 @@
---
name: openspec-new-change
description: Start a new OpenSpec change using the experimental artifact workflow. Use when the user wants to create a new feature, fix, or modification with a structured step-by-step approach.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Start a new change using the experimental artifact-driven approach.
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
**Steps**
1. **If no clear input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Determine the workflow schema**
Use the default schema (omit `--schema`) unless the user explicitly requests a different workflow.
**Use a different schema only if the user mentions:**
- A specific schema name → use `--schema <name>`
- "show workflows" or "what workflows" → run `openspec schemas --json` and let them choose
**Otherwise**: Omit `--schema` to use the default.
3. **Create the change directory**
```bash
openspec new change "<name>"
```
Add `--schema <name>` only if the user requested a specific workflow.
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
4. **Show the artifact status**
```bash
openspec status --change "<name>"
```
This shows which artifacts need to be created and which are ready (dependencies satisfied).
5. **Get instructions for the first artifact**
The first artifact depends on the schema (e.g., `proposal` for spec-driven).
Check the status output to find the first artifact with status "ready".
```bash
openspec instructions <first-artifact-id> --change "<name>"
```
This outputs the template and context for creating the first artifact.
6. **STOP and wait for user direction**
**Output**
After completing the steps, summarize:
- Change name and location
- Schema/workflow being used and its artifact sequence
- Current status (0/N artifacts complete)
- The template for the first artifact
- Prompt: "Ready to create the first artifact? Just describe what this change is about and I'll draft it, or ask me to continue."
**Guardrails**
- Do NOT create any artifacts yet - just show the instructions
- Do NOT advance beyond showing the first artifact template
- If the name is invalid (not kebab-case), ask for a valid name
- If a change with that name already exists, suggest continuing that change instead
- Pass --schema if using a non-default workflow

View File

@@ -0,0 +1,554 @@
---
name: openspec-onboard
description: Guided onboarding for OpenSpec - walk through a complete workflow cycle with narration and real codebase work.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step.
---
## Preflight
Before starting, check if the OpenSpec CLI is installed:
```bash
# Unix/macOS
openspec --version 2>&1 || echo "CLI_NOT_INSTALLED"
# Windows (PowerShell)
# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" }
```
**If CLI not installed:**
> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`.
Stop here if not installed.
---
## Phase 1: Welcome
Display:
```
## Welcome to OpenSpec!
I'll walk you through a complete change cycle—from idea to implementation—using a real task in your codebase. Along the way, you'll learn the workflow by doing it.
**What we'll do:**
1. Pick a small, real task in your codebase
2. Explore the problem briefly
3. Create a change (the container for our work)
4. Build the artifacts: proposal → specs → design → tasks
5. Implement the tasks
6. Archive the completed change
**Time:** ~15-20 minutes
Let's start by finding something to work on.
```
---
## Phase 2: Task Selection
### Codebase Analysis
Scan the codebase for small improvement opportunities. Look for:
1. **TODO/FIXME comments** - Search for `TODO`, `FIXME`, `HACK`, `XXX` in code files
2. **Missing error handling** - `catch` blocks that swallow errors, risky operations without try-catch
3. **Functions without tests** - Cross-reference `src/` with test directories
4. **Type issues** - `any` types in TypeScript files (`: any`, `as any`)
5. **Debug artifacts** - `console.log`, `console.debug`, `debugger` statements in non-debug code
6. **Missing validation** - User input handlers without validation
Also check recent git activity:
```bash
# Unix/macOS
git log --oneline -10 2>/dev/null || echo "No git history"
# Windows (PowerShell)
# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" }
```
### Present Suggestions
From your analysis, present 3-4 specific suggestions:
```
## Task Suggestions
Based on scanning your codebase, here are some good starter tasks:
**1. [Most promising task]**
Location: `src/path/to/file.ts:42`
Scope: ~1-2 files, ~20-30 lines
Why it's good: [brief reason]
**2. [Second task]**
Location: `src/another/file.ts`
Scope: ~1 file, ~15 lines
Why it's good: [brief reason]
**3. [Third task]**
Location: [location]
Scope: [estimate]
Why it's good: [brief reason]
**4. Something else?**
Tell me what you'd like to work on.
Which task interests you? (Pick a number or describe your own)
```
**If nothing found:** Fall back to asking what the user wants to build:
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
### Scope Guardrail
If the user picks or describes something too large (major feature, multi-day work):
```
That's a valuable task, but it's probably larger than ideal for your first OpenSpec run-through.
For learning the workflow, smaller is better—it lets you see the full cycle without getting stuck in implementation details.
**Options:**
1. **Slice it smaller** - What's the smallest useful piece of [their task]? Maybe just [specific slice]?
2. **Pick something else** - One of the other suggestions, or a different small task?
3. **Do it anyway** - If you really want to tackle this, we can. Just know it'll take longer.
What would you prefer?
```
Let the user override if they insist—this is a soft guardrail.
---
## Phase 3: Explore Demo
Once a task is selected, briefly demonstrate explore mode:
```
Before we create a change, let me quickly show you **explore mode**—it's how you think through problems before committing to a direction.
```
Spend 1-2 minutes investigating the relevant code:
- Read the file(s) involved
- Draw a quick ASCII diagram if it helps
- Note any considerations
```
## Quick Exploration
[Your brief analysis—what you found, any considerations]
┌─────────────────────────────────────────┐
│ [Optional: ASCII diagram if helpful] │
└─────────────────────────────────────────┘
Explore mode (`/opsx:explore`) is for this kind of thinking—investigating before implementing. You can use it anytime you need to think through a problem.
Now let's create a change to hold our work.
```
**PAUSE** - Wait for user acknowledgment before proceeding.
---
## Phase 4: Create the Change
**EXPLAIN:**
```
## Creating a Change
A "change" in OpenSpec is a container for all the thinking and planning around a piece of work. It lives in `openspec/changes/<name>/` and holds your artifacts—proposal, specs, design, tasks.
Let me create one for our task.
```
**DO:** Create the change with a derived kebab-case name:
```bash
openspec new change "<derived-name>"
```
**SHOW:**
```
Created: `openspec/changes/<name>/`
The folder structure:
```
openspec/changes/<name>/
├── proposal.md ← Why we're doing this (empty, we'll fill it)
├── design.md ← How we'll build it (empty)
├── specs/ ← Detailed requirements (empty)
└── tasks.md ← Implementation checklist (empty)
```
Now let's fill in the first artifact—the proposal.
```
---
## Phase 5: Proposal
**EXPLAIN:**
```
## The Proposal
The proposal captures **why** we're making this change and **what** it involves at a high level. It's the "elevator pitch" for the work.
I'll draft one based on our task.
```
**DO:** Draft the proposal content (don't save yet):
```
Here's a draft proposal:
---
## Why
[1-2 sentences explaining the problem/opportunity]
## What Changes
[Bullet points of what will be different]
## Capabilities
### New Capabilities
- `<capability-name>`: [brief description]
### Modified Capabilities
<!-- If modifying existing behavior -->
## Impact
- `src/path/to/file.ts`: [what changes]
- [other files if applicable]
---
Does this capture the intent? I can adjust before we save it.
```
**PAUSE** - Wait for user approval/feedback.
After approval, save the proposal:
```bash
openspec instructions proposal --change "<name>" --json
```
Then write the content to `openspec/changes/<name>/proposal.md`.
```
Proposal saved. This is your "why" document—you can always come back and refine it as understanding evolves.
Next up: specs.
```
---
## Phase 6: Specs
**EXPLAIN:**
```
## Specs
Specs define **what** we're building in precise, testable terms. They use a requirement/scenario format that makes expected behavior crystal clear.
For a small task like this, we might only need one spec file.
```
**DO:** Create the spec file:
```bash
# Unix/macOS
mkdir -p openspec/changes/<name>/specs/<capability-name>
# Windows (PowerShell)
# New-Item -ItemType Directory -Force -Path "openspec/changes/<name>/specs/<capability-name>"
```
Draft the spec content:
```
Here's the spec:
---
## ADDED Requirements
### Requirement: <Name>
<Description of what the system should do>
#### Scenario: <Scenario name>
- **WHEN** <trigger condition>
- **THEN** <expected outcome>
- **AND** <additional outcome if needed>
---
This format—WHEN/THEN/AND—makes requirements testable. You can literally read them as test cases.
```
Save to `openspec/changes/<name>/specs/<capability>/spec.md`.
---
## Phase 7: Design
**EXPLAIN:**
```
## Design
The design captures **how** we'll build it—technical decisions, tradeoffs, approach.
For small changes, this might be brief. That's fine—not every change needs deep design discussion.
```
**DO:** Draft design.md:
```
Here's the design:
---
## Context
[Brief context about the current state]
## Goals / Non-Goals
**Goals:**
- [What we're trying to achieve]
**Non-Goals:**
- [What's explicitly out of scope]
## Decisions
### Decision 1: [Key decision]
[Explanation of approach and rationale]
---
For a small task, this captures the key decisions without over-engineering.
```
Save to `openspec/changes/<name>/design.md`.
---
## Phase 8: Tasks
**EXPLAIN:**
```
## Tasks
Finally, we break the work into implementation tasks—checkboxes that drive the apply phase.
These should be small, clear, and in logical order.
```
**DO:** Generate tasks based on specs and design:
```
Here are the implementation tasks:
---
## 1. [Category or file]
- [ ] 1.1 [Specific task]
- [ ] 1.2 [Specific task]
## 2. Verify
- [ ] 2.1 [Verification step]
---
Each checkbox becomes a unit of work in the apply phase. Ready to implement?
```
**PAUSE** - Wait for user to confirm they're ready to implement.
Save to `openspec/changes/<name>/tasks.md`.
---
## Phase 9: Apply (Implementation)
**EXPLAIN:**
```
## Implementation
Now we implement each task, checking them off as we go. I'll announce each one and occasionally note how the specs/design informed the approach.
```
**DO:** For each task:
1. Announce: "Working on task N: [description]"
2. Implement the change in the codebase
3. Reference specs/design naturally: "The spec says X, so I'm doing Y"
4. Mark complete in tasks.md: `- [ ]``- [x]`
5. Brief status: "✓ Task N complete"
Keep narration light—don't over-explain every line of code.
After all tasks:
```
## Implementation Complete
All tasks done:
- [x] Task 1
- [x] Task 2
- [x] ...
The change is implemented! One more step—let's archive it.
```
---
## Phase 10: Archive
**EXPLAIN:**
```
## Archiving
When a change is complete, we archive it. This moves it from `openspec/changes/` to `openspec/changes/archive/YYYY-MM-DD-<name>/`.
Archived changes become your project's decision history—you can always find them later to understand why something was built a certain way.
```
**DO:**
```bash
openspec archive "<name>"
```
**SHOW:**
```
Archived to: `openspec/changes/archive/YYYY-MM-DD-<name>/`
The change is now part of your project's history. The code is in your codebase, the decision record is preserved.
```
---
## Phase 11: Recap & Next Steps
```
## Congratulations!
You just completed a full OpenSpec cycle:
1. **Explore** - Thought through the problem
2. **New** - Created a change container
3. **Proposal** - Captured WHY
4. **Specs** - Defined WHAT in detail
5. **Design** - Decided HOW
6. **Tasks** - Broke it into steps
7. **Apply** - Implemented the work
8. **Archive** - Preserved the record
This same rhythm works for any size change—a small fix or a major feature.
---
## Command Reference
**Core workflow:**
| Command | What it does |
|---------|--------------|
| `/opsx:propose` | Create a change and generate all artifacts |
| `/opsx:explore` | Think through problems before/during work |
| `/opsx:apply` | Implement tasks from a change |
| `/opsx:archive` | Archive a completed change |
**Additional commands:**
| Command | What it does |
|---------|--------------|
| `/opsx:new` | Start a new change, step through artifacts one at a time |
| `/opsx:continue` | Continue working on an existing change |
| `/opsx:ff` | Fast-forward: create all artifacts at once |
| `/opsx:verify` | Verify implementation matches artifacts |
---
## What's Next?
Try `/opsx:propose` on something you actually want to build. You've got the rhythm now!
```
---
## Graceful Exit Handling
### User wants to stop mid-way
If the user says they need to stop, want to pause, or seem disengaged:
```
No problem! Your change is saved at `openspec/changes/<name>/`.
To pick up where we left off later:
- `/opsx:continue <name>` - Resume artifact creation
- `/opsx:apply <name>` - Jump to implementation (if tasks exist)
The work won't be lost. Come back whenever you're ready.
```
Exit gracefully without pressure.
### User just wants command reference
If the user says they just want to see the commands or skip the tutorial:
```
## OpenSpec Quick Reference
**Core workflow:**
| Command | What it does |
|---------|--------------|
| `/opsx:propose <name>` | Create a change and generate all artifacts |
| `/opsx:explore` | Think through problems (no code changes) |
| `/opsx:apply <name>` | Implement tasks |
| `/opsx:archive <name>` | Archive when done |
**Additional commands:**
| Command | What it does |
|---------|--------------|
| `/opsx:new <name>` | Start a new change, step by step |
| `/opsx:continue <name>` | Continue an existing change |
| `/opsx:ff <name>` | Fast-forward: all artifacts at once |
| `/opsx:verify <name>` | Verify implementation |
Try `/opsx:propose` to start your first change.
```
Exit gracefully.
---
## Guardrails
- **Follow the EXPLAIN → DO → SHOW → PAUSE pattern** at key transitions (after explore, after proposal draft, after tasks, after archive)
- **Keep narration light** during implementation—teach without lecturing
- **Don't skip phases** even if the change is small—the goal is teaching the workflow
- **Pause for acknowledgment** at marked points, but don't over-pause
- **Handle exits gracefully**—never pressure the user to continue
- **Use real codebase tasks**—don't simulate or use fake examples
- **Adjust scope gently**—guide toward smaller tasks but respect user choice

View File

@@ -0,0 +1,138 @@
---
name: openspec-sync-specs
description: Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Sync delta specs from a change to main specs.
This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show changes that have delta specs (under `specs/` directory).
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Find delta specs**
Look for delta spec files in `openspec/changes/<name>/specs/*/spec.md`.
Each delta spec file contains sections like:
- `## ADDED Requirements` - New requirements to add
- `## MODIFIED Requirements` - Changes to existing requirements
- `## REMOVED Requirements` - Requirements to remove
- `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
If no delta specs found, inform user and stop.
3. **For each delta spec, apply changes to main specs**
For each capability with a delta spec at `openspec/changes/<name>/specs/<capability>/spec.md`:
a. **Read the delta spec** to understand the intended changes
b. **Read the main spec** at `openspec/specs/<capability>/spec.md` (may not exist yet)
c. **Apply changes intelligently**:
**ADDED Requirements:**
- If requirement doesn't exist in main spec → add it
- If requirement already exists → update it to match (treat as implicit MODIFIED)
**MODIFIED Requirements:**
- Find the requirement in main spec
- Apply the changes - this can be:
- Adding new scenarios (don't need to copy existing ones)
- Modifying existing scenarios
- Changing the requirement description
- Preserve scenarios/content not mentioned in the delta
**REMOVED Requirements:**
- Remove the entire requirement block from main spec
**RENAMED Requirements:**
- Find the FROM requirement, rename to TO
d. **Create new main spec** if capability doesn't exist yet:
- Create `openspec/specs/<capability>/spec.md`
- Add Purpose section (can be brief, mark as TBD)
- Add Requirements section with the ADDED requirements
4. **Show summary**
After applying all changes, summarize:
- Which capabilities were updated
- What changes were made (requirements added/modified/removed/renamed)
**Delta Spec Format Reference**
```markdown
## ADDED Requirements
### Requirement: New Feature
The system SHALL do something new.
#### Scenario: Basic case
- **WHEN** user does X
- **THEN** system does Y
## MODIFIED Requirements
### Requirement: Existing Feature
#### Scenario: New scenario to add
- **WHEN** user does A
- **THEN** system does B
## REMOVED Requirements
### Requirement: Deprecated Feature
## RENAMED Requirements
- FROM: `### Requirement: Old Name`
- TO: `### Requirement: New Name`
```
**Key Principle: Intelligent Merging**
Unlike programmatic merging, you can apply **partial updates**:
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
- The delta represents *intent*, not a wholesale replacement
- Use your judgment to merge changes sensibly
**Output On Success**
```
## Specs Synced: <change-name>
Updated main specs:
**<capability-1>**:
- Added requirement: "New Feature"
- Modified requirement: "Existing Feature" (added 1 scenario)
**<capability-2>**:
- Created new spec file
- Added requirement: "Another Feature"
Main specs are now updated. The change remains active - archive when implementation is complete.
```
**Guardrails**
- Read both delta and main specs before making changes
- Preserve existing content not mentioned in delta
- If something is unclear, ask for clarification
- Show what you're changing as you go
- The operation should be idempotent - running twice should give same result

View File

@@ -0,0 +1,168 @@
---
name: openspec-verify-change
description: Verify implementation matches change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.2.0"
---
Verify that an implementation matches the change artifacts (specs, tasks, design).
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1. **If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show changes that have implementation tasks (tasks artifact exists).
Include the schema used for each change if available.
Mark changes with incomplete tasks as "(In Progress)".
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- Which artifacts exist for this change
3. **Get the change directory and load artifacts**
```bash
openspec instructions apply --change "<name>" --json
```
This returns the change directory and context files. Read all available artifacts from `contextFiles`.
4. **Initialize verification report structure**
Create a report structure with three dimensions:
- **Completeness**: Track tasks and spec coverage
- **Correctness**: Track requirement implementation and scenario coverage
- **Coherence**: Track design adherence and pattern consistency
Each dimension can have CRITICAL, WARNING, or SUGGESTION issues.
5. **Verify Completeness**
**Task Completion**:
- If tasks.md exists in contextFiles, read it
- Parse checkboxes: `- [ ]` (incomplete) vs `- [x]` (complete)
- Count complete vs total tasks
- If incomplete tasks exist:
- Add CRITICAL issue for each incomplete task
- Recommendation: "Complete task: <description>" or "Mark as done if already implemented"
**Spec Coverage**:
- If delta specs exist in `openspec/changes/<name>/specs/`:
- Extract all requirements (marked with "### Requirement:")
- For each requirement:
- Search codebase for keywords related to the requirement
- Assess if implementation likely exists
- If requirements appear unimplemented:
- Add CRITICAL issue: "Requirement not found: <requirement name>"
- Recommendation: "Implement requirement X: <description>"
6. **Verify Correctness**
**Requirement Implementation Mapping**:
- For each requirement from delta specs:
- Search codebase for implementation evidence
- If found, note file paths and line ranges
- Assess if implementation matches requirement intent
- If divergence detected:
- Add WARNING: "Implementation may diverge from spec: <details>"
- Recommendation: "Review <file>:<lines> against requirement X"
**Scenario Coverage**:
- For each scenario in delta specs (marked with "#### Scenario:"):
- Check if conditions are handled in code
- Check if tests exist covering the scenario
- If scenario appears uncovered:
- Add WARNING: "Scenario not covered: <scenario name>"
- Recommendation: "Add test or implementation for scenario: <description>"
7. **Verify Coherence**
**Design Adherence**:
- If design.md exists in contextFiles:
- Extract key decisions (look for sections like "Decision:", "Approach:", "Architecture:")
- Verify implementation follows those decisions
- If contradiction detected:
- Add WARNING: "Design decision not followed: <decision>"
- Recommendation: "Update implementation or revise design.md to match reality"
- If no design.md: Skip design adherence check, note "No design.md to verify against"
**Code Pattern Consistency**:
- Review new code for consistency with project patterns
- Check file naming, directory structure, coding style
- If significant deviations found:
- Add SUGGESTION: "Code pattern deviation: <details>"
- Recommendation: "Consider following project pattern: <example>"
8. **Generate Verification Report**
**Summary Scorecard**:
```
## Verification Report: <change-name>
### Summary
| Dimension | Status |
|--------------|------------------|
| Completeness | X/Y tasks, N reqs|
| Correctness | M/N reqs covered |
| Coherence | Followed/Issues |
```
**Issues by Priority**:
1. **CRITICAL** (Must fix before archive):
- Incomplete tasks
- Missing requirement implementations
- Each with specific, actionable recommendation
2. **WARNING** (Should fix):
- Spec/design divergences
- Missing scenario coverage
- Each with specific recommendation
3. **SUGGESTION** (Nice to fix):
- Pattern inconsistencies
- Minor improvements
- Each with specific recommendation
**Final Assessment**:
- If CRITICAL issues: "X critical issue(s) found. Fix before archiving."
- If only warnings: "No critical issues. Y warning(s) to consider. Ready for archive (with noted improvements)."
- If all clear: "All checks passed. Ready for archive."
**Verification Heuristics**
- **Completeness**: Focus on objective checklist items (checkboxes, requirements list)
- **Correctness**: Use keyword search, file path analysis, reasonable inference - don't require perfect certainty
- **Coherence**: Look for glaring inconsistencies, don't nitpick style
- **False Positives**: When uncertain, prefer SUGGESTION over WARNING, WARNING over CRITICAL
- **Actionability**: Every issue must have a specific recommendation with file/line references where applicable
**Graceful Degradation**
- If only tasks.md exists: verify task completion only, skip spec/design checks
- If tasks + specs exist: verify completeness and correctness, skip design
- If full artifacts: verify all three dimensions
- Always note which checks were skipped and why
**Output Format**
Use clear markdown with:
- Table for summary scorecard
- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION)
- Code references in format: `file.ts:123`
- Specific, actionable recommendations
- No vague suggestions like "consider reviewing"

146
CLAUDE.md
View File

@@ -1,147 +1,5 @@
<!-- GSD:project-start source:PROJECT.md -->
## Project ## Project
**Chat Agent WebApp** Chat Agent WebApp — a personal AI chat app built with Blazor WebAssembly and the OpenAI GPT API that doubles as an incremental Blazor tutorial.
A personal AI chat web application built with Blazor WebAssembly and the OpenAI GPT API. Users send messages, receive streaming AI responses rendered as markdown, and manage multiple persistent conversations. The project doubles as an incremental learning journey — each phase introduces one concept with well-documented, explained code, making it suitable as a Blazor tutorial for a developer experienced in C# but new to the framework. For full project details, constraints, and technology stack, see `openspec/specs/`.
**Core Value:** A working, well-understood AI chat interface — every line of code is intentional and explained, so the builder learns Blazor patterns while shipping a real product.
### Constraints
- **Tech stack**: .NET / C# / Blazor WebAssembly — non-negotiable
- **LLM provider**: OpenAI GPT API
- **Storage**: JSON files on local disk
- **Architecture**: WASM client + backend API (API key stays server-side)
- **Code style**: Every Blazor concept introduced must have inline comments explaining what it does and why
<!-- GSD:project-end -->
<!-- GSD:stack-start source:research/STACK.md -->
## Technology Stack
## Recommended Stack
### Core Technologies
| Technology | Version | Purpose | Why Recommended |
|------------|---------|---------|-----------------|
| .NET 9 SDK | 9.x (latest patch) | Runtime, tooling, SDK | LTS-adjacent, stable, .NET 10 is in preview — stay on 9 for a tutorial project targeting a stable foundation |
| Blazor WebAssembly Standalone | .NET 9 | Client SPA running in-browser | Non-negotiable per project constraints; client-side execution with no server round-trip for UI |
| ASP.NET Core Web API | .NET 9 | Backend proxy for OpenAI calls | Required to keep the OpenAI API key server-side; WASM cannot access secrets directly |
| C# 13 | Included with .NET 9 | Application language | Included in .NET 9 SDK; no separate install needed |
### OpenAI Integration
| Library | Version | Purpose | Why Recommended |
|---------|---------|---------|-----------------|
| `OpenAI` (official) | 2.9.1 | OpenAI API client with streaming | The official OpenAI-published .NET library; supports `CompleteChatStreamingAsync()` returning `AsyncCollectionResult<StreamingChatCompletionUpdate>` via `await foreach`; stable release as of 2026-03-02 |
### Markdown Rendering
| Library | Version | Purpose | Why Recommended |
|---------|---------|---------|-----------------|
| `Markdig` | 1.1.1 | Parse markdown text to HTML | The de facto standard markdown processor for .NET; CommonMark-compliant, fast, extensible, targets .NET Standard 2.0 so works in WASM; used by Microsoft and Syncfusion as the underlying engine |
### UI Component Library
| Library | Version | Purpose | Why Recommended |
|---------|---------|---------|-----------------|
| `MudBlazor` | 9.2.0 | Material Design component library | Full .NET 9 support confirmed; pure C# with minimal JavaScript; comprehensive chat-friendly components (MudTextField, MudPaper, MudScrollToBottom, MudList); large community; no per-seat licensing |
### JSON Storage (Server-side)
| Technology | Version | Purpose | Why Recommended |
|------------|---------|---------|-----------------|
| `System.Text.Json` | Built into .NET 9 | Serialize/deserialize conversation history | Built-in, no extra dependency; `JsonSerializerOptions` with `WriteIndented = true` for human-readable files; async file I/O via `File.ReadAllTextAsync` / `File.WriteAllTextAsync` |
## Supporting Libraries
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| `Microsoft.Extensions.AI` (abstractions) | 9.x preview | Optional AI abstraction layer | Skip for v1 — adds indirection before the core chat pattern is understood. Relevant for v2 when adding multi-provider support |
| `Blazored.LocalStorage` | latest | Browser local storage | Not needed for this project — persistence is on the server via JSON files, not the browser |
| `System.Net.ServerSentEvents` | Built into .NET 9 | SSE parser for streaming | Used automatically by the `OpenAI` library on the server; no direct usage needed |
## Development Tools
| Tool | Purpose | Notes |
|------|---------|-------|
| Visual Studio 2022 (v17.12+) | IDE with Blazor hot reload | Recommended for tutorial builder; full Blazor debugging, component preview, and hot reload support |
| VS Code + C# Dev Kit | Lighter-weight alternative | Works well; use `dotnet watch` for hot reload |
| `dotnet watch run` | Hot reload during development | Run in both Client and Server project directories simultaneously |
| `dotnet-dev-certs` | HTTPS dev certificate | Required for local HTTPS; run `dotnet dev-certs https --trust` once |
## Installation
# Create solution
# Create Blazor WASM client (standalone)
# Create ASP.NET Core Web API backend
# Install OpenAI SDK in the API project
# Install Markdig in the Client project
## Alternatives Considered
| Recommended | Alternative | When to Use Alternative |
|-------------|-------------|-------------------------|
| `OpenAI` 2.9.1 (official) | `OpenAI-DotNet` 8.8.8 (unofficial) | Never — the official package is now stable and maintained by OpenAI directly |
| `OpenAI` 2.9.1 (official) | `Azure.AI.OpenAI` 2.1.0 | When targeting Azure OpenAI Service specifically (e.g., enterprise, EU data residency, private endpoints) — overkill for this project |
| `Markdig` | `CommonMark.NET` | Only if strict CommonMark compliance matters more than extensions; Markdig is a superset and the ecosystem standard |
| `MudBlazor` | Radzen Blazor | Radzen is fine; choose it if you already know it; MudBlazor has more learning resources |
| `MudBlazor` | Telerik UI for Blazor | Telerik requires a paid license; not appropriate for a personal tool |
| Standalone WASM + separate Web API | Blazor Web App template (unified) | Use the unified Blazor Web App template when you want mixed Server+WASM render modes on a single project; overkill for this project and obscures the WASM-specific patterns the tutorial aims to teach |
| JSON flat files (server-side) | SQLite via EF Core | SQLite is a better choice at scale; JSON is simpler for single-user personal tools and avoids introducing a migration workflow |
## What NOT to Use
| Avoid | Why | Use Instead |
|-------|-----|-------------|
| `OpenAI-DotNet` (unofficial) | Different API surface, not maintained by OpenAI, version numbers create confusion | Official `OpenAI` NuGet package |
| `Microsoft.SemanticKernel` | Adds significant abstraction and dependency weight for a tutorial; streaming works but is complex to explain | Direct `OpenAI` SDK calls; add SK in v2 when orchestration is needed |
| JavaScript `EventSource` API via JSInterop for streaming | Blazor WASM has `SetBrowserResponseStreamingEnabled` which avoids JS interop; adding JSInterop for streaming increases complexity significantly | `HttpCompletionOption.ResponseHeadersRead` + `SetBrowserResponseStreamingEnabled(true)` in the HTTP handler |
| `Newtonsoft.Json` | Unnecessary dependency; `System.Text.Json` is built into .NET 9 and is faster; Newtonsoft was the pre-.NET Core standard | `System.Text.Json` (built-in) |
| `Blazored.LocalStorage` for persistence | Browser storage is limited (~5MB), cleared by users, and not suitable for chat history of any meaningful length; also exposes all data client-side | Server-side JSON file storage via the Web API |
| AOT compilation during learning phase | Dramatically increases build times; not needed until production optimization is a concern; confusing to introduce in a tutorial | Default IL interpretation; add AOT opt-in note in the final phase |
## Stack Patterns by Variant
- Backend streams OpenAI tokens as `text/event-stream` (SSE) or `application/x-ndjson`
- Client uses `SetBrowserResponseStreamingEnabled(true)` on `HttpRequestMessage`
- Client reads with `HttpCompletionOption.ResponseHeadersRead` and iterates the stream
- Trigger `StateHasChanged()` in the component after each token to update the UI
- Define a `ConversationRepository` service on the API that reads/writes from a configurable base path
- Register as `Singleton` (not `Scoped`) since there is only one user and file access must be serialized
- Use `SemaphoreSlim(1,1)` to prevent concurrent write conflicts even in single-user mode
- Use `Markdig.Markdown.ToHtml(text, pipeline)` where `pipeline` is built with `MarkdownPipelineBuilder` enabling extensions (e.g., `UseAutoLinks()`, `UseEmojiAndSmiley()`)
- Render the HTML string using `@((MarkupString)html)` inside a `<div class="markdown-body">` element
- Apply CSS (GitHub Markdown CSS or custom) scoped to `.markdown-body` for code blocks and tables
## Version Compatibility
| Package | Compatible With | Notes |
|---------|-----------------|-------|
| `OpenAI` 2.9.1 | .NET Standard 2.0+ (.NET 9 confirmed) | Published 2026-03-02; requires `System.Net.ServerSentEvents` (built into .NET 9) |
| `Markdig` 1.1.1 | .NET 8.0, .NET Standard 2.0, .NET Framework 4.6.2 | .NET 9 compatible via .NET 8 TFM; published 2026-03-04 |
| `MudBlazor` 9.2.0 | .NET 8.0, .NET 9.0, .NET 10.0 | Published 2026-03-18; version 9.x = full support for .NET 9 |
| .NET 9 SDK | Blazor WASM + Web API in same solution | Both project types target `net9.0`; no cross-framework issues |
## Sources
- https://www.nuget.org/packages/OpenAI — Official OpenAI NuGet package; version 2.9.1 confirmed (2026-03-02)
- https://github.com/openai/openai-dotnet — Official OpenAI .NET SDK; streaming API verified (`CompleteChatStreamingAsync`, `await foreach`)
- https://www.nuget.org/packages/Markdig — Markdig version 1.1.1 confirmed (2026-03-04)
- https://www.nuget.org/packages/MudBlazor — MudBlazor 9.2.0 confirmed; .NET 8/9/10 full support (2026-03-18)
- https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-9.0 — Official Blazor hosting model docs; standalone WASM vs Blazor Web App distinction verified
- https://learn.microsoft.com/en-us/dotnet/core/compatibility/networking/10.0/default-http-streaming — Breaking change: WASM streaming opt-in (.NET 9) vs default (.NET 10)
- https://www.strathweb.com/2024/07/built-in-support-for-server-sent-events-in-net-9/ — SSE native support in .NET 9 via `System.Net.ServerSentEvents`; used internally by OpenAI SDK (MEDIUM confidence, single source)
- https://github.com/openai/openai-dotnet/issues/65 — Confirmed streaming issue in Blazor WASM requires `SetBrowserResponseStreamingEnabled(true)` (MEDIUM confidence, GitHub issue thread)
- https://devblogs.microsoft.com/dotnet/openai-dotnet-library/ — Official .NET Blog announcement of the OpenAI library
- https://dev.to/kazinix/blazor-web-app-webassembly-hosted-in-net8-and-net9-1k6g — Hosted template removal in .NET 8+, manual solution structure (MEDIUM confidence)
<!-- GSD:stack-end -->
<!-- GSD:conventions-start source:CONVENTIONS.md -->
## Conventions
Conventions not yet established. Will populate as patterns emerge during development.
<!-- GSD:conventions-end -->
<!-- GSD:architecture-start source:ARCHITECTURE.md -->
## Architecture
Architecture not yet mapped. Follow existing patterns found in the codebase.
<!-- GSD:architecture-end -->
<!-- GSD:workflow-start source:GSD defaults -->
## GSD 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.
<!-- GSD:workflow-end -->
<!-- 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 -->

View File

@@ -11,6 +11,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatAgent.Api", "src\ChatAg
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatAgent.Shared", "src\ChatAgent.Shared\ChatAgent.Shared.csproj", "{06182E3F-BC78-449B-ADF6-D9EE49E48945}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatAgent.Shared", "src\ChatAgent.Shared\ChatAgent.Shared.csproj", "{06182E3F-BC78-449B-ADF6-D9EE49E48945}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatAgent.Api.Tests", "tests\ChatAgent.Api.Tests\ChatAgent.Api.Tests.csproj", "{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatAgent.Client.Tests", "tests\ChatAgent.Client.Tests\ChatAgent.Client.Tests.csproj", "{DBB73B66-042A-4858-B813-23AEA84FC4C6}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -57,6 +63,30 @@ Global
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|x64.Build.0 = Release|Any CPU {06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|x64.Build.0 = Release|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|x86.ActiveCfg = Release|Any CPU {06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|x86.ActiveCfg = Release|Any CPU
{06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|x86.Build.0 = Release|Any CPU {06182E3F-BC78-449B-ADF6-D9EE49E48945}.Release|x86.Build.0 = Release|Any CPU
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Debug|x64.ActiveCfg = Debug|Any CPU
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Debug|x64.Build.0 = Debug|Any CPU
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Debug|x86.ActiveCfg = Debug|Any CPU
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Debug|x86.Build.0 = Debug|Any CPU
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Release|Any CPU.Build.0 = Release|Any CPU
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Release|x64.ActiveCfg = Release|Any CPU
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Release|x64.Build.0 = Release|Any CPU
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Release|x86.ActiveCfg = Release|Any CPU
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5}.Release|x86.Build.0 = Release|Any CPU
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Debug|x64.ActiveCfg = Debug|Any CPU
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Debug|x64.Build.0 = Debug|Any CPU
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Debug|x86.ActiveCfg = Debug|Any CPU
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Debug|x86.Build.0 = Debug|Any CPU
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Release|Any CPU.Build.0 = Release|Any CPU
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Release|x64.ActiveCfg = Release|Any CPU
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Release|x64.Build.0 = Release|Any CPU
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Release|x86.ActiveCfg = Release|Any CPU
{DBB73B66-042A-4858-B813-23AEA84FC4C6}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -65,5 +95,7 @@ Global
{600EA0C4-7CDE-4807-BE3C-30A6D2242392} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {600EA0C4-7CDE-4807-BE3C-30A6D2242392} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{467D4550-6F9A-456E-B99C-0ABE94070ECF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {467D4550-6F9A-456E-B99C-0ABE94070ECF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{06182E3F-BC78-449B-ADF6-D9EE49E48945} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {06182E3F-BC78-449B-ADF6-D9EE49E48945} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{C70B5EEA-073A-4ED0-BA02-C4A92583ECE5} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{DBB73B66-042A-4858-B813-23AEA84FC4C6} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@@ -0,0 +1,25 @@
<html>
<body>
<p>Subject: RE: AG Inflation swaps CVA Request</p>
<p>Ovi,</p>
<p>Hope you are well.</p>
<p>Could you kindly share indicative CVA for the below two inflation swaps, assuming the counterparty is Assured Guaranty UK Limited (formerly Assured Guaranty (Europe) Ltd), which is rated AA- by S&amp;P and A1 by Moody's please?</p>
<p>OB 27/11/2025</p>
<p><b>Swap 1 please price standalone</b></p>
<table border="1">
<tr><th></th><th>CSA</th><th>Murex</th><th>PV (£)</th></tr>
<tr><td>Coupon Leg</td><td>BTMU_JPY</td><td>79353083</td><td>4,562,456</td></tr>
<tr><td>APD leg</td><td>BTMU_JPY</td><td>79353084</td><td>76,985,170</td></tr>
</table>
<p>Total PV: 81,547,626</p>
<p><b>Swap 2 please price standalone</b></p>
<table border="1">
<tr><th></th><th>CSA</th><th>Murex</th><th>PV (£)</th></tr>
<tr><td>Coupon Leg</td><td>BTMU_JPY</td><td>79353093</td><td>1,663,261</td></tr>
<tr><td>APD leg</td><td>BTMU_JPY</td><td>79353094</td><td>41,333,773</td></tr>
</table>
<p>Total PV: 42,997,034</p>
<p>Many thanks.</p>
<p>Kind regards,</p>
</body>
</html>

View File

@@ -0,0 +1,36 @@
{
"items": [
{
"valuedate": "27/11/2025",
"counterparty": "Assured Guaranty UK Limited (formerly Assured Guaranty (Europe) Ltd)",
"trade_id": 79353083,
"display_ccy": "GBP",
"pv": 4562456,
"breakclause": "N"
},
{
"valuedate": "27/11/2025",
"counterparty": "Assured Guaranty UK Limited (formerly Assured Guaranty (Europe) Ltd)",
"trade_id": 79353084,
"display_ccy": "GBP",
"pv": 76985170,
"breakclause": "N"
},
{
"valuedate": "27/11/2025",
"counterparty": "Assured Guaranty UK Limited (formerly Assured Guaranty (Europe) Ltd)",
"trade_id": 79353093,
"display_ccy": "GBP",
"pv": 1663261,
"breakclause": "N"
},
{
"valuedate": "27/11/2025",
"counterparty": "Assured Guaranty UK Limited (formerly Assured Guaranty (Europe) Ltd)",
"trade_id": 79353094,
"display_ccy": "GBP",
"pv": 41333773,
"breakclause": "N"
}
]
}

View File

@@ -0,0 +1,14 @@
<html>
<body>
<p>Subject: CVA quote USD interest rate swap</p>
<p>Hi team,</p>
<p>Please provide CVA for the following single interest rate swap with Deutsche Bank AG, London Branch.</p>
<p>Value date: 15/03/2026</p>
<table border="1">
<tr><th></th><th>CSA</th><th>Murex</th><th>PV ($)</th></tr>
<tr><td>Fixed Leg</td><td>DB_USD</td><td>81200451</td><td>12,750,000</td></tr>
</table>
<p>Thanks,</p>
<p>Sarah</p>
</body>
</html>

View File

@@ -0,0 +1,12 @@
{
"items": [
{
"valuedate": "15/03/2026",
"counterparty": "Deutsche Bank AG, London Branch",
"trade_id": 81200451,
"display_ccy": "USD",
"pv": 12750000,
"breakclause": "N"
}
]
}

View File

@@ -0,0 +1,15 @@
<html>
<body>
<p>Subject: RE: CVA Cross-currency swap with break clause</p>
<p>Dear Ovi,</p>
<p>Could you provide indicative CVA for the below cross-currency swap with Barclays Bank PLC? Note: this trade has a break clause at the 5-year point.</p>
<p>OB 01/06/2025</p>
<table border="1">
<tr><th></th><th>CSA</th><th>Murex</th><th>PV (€)</th></tr>
<tr><td>EUR Leg</td><td>BARC_EUR</td><td>77890112</td><td>8,421,300</td></tr>
<tr><td>GBP Leg</td><td>BARC_EUR</td><td>77890113</td><td>22,105,800</td></tr>
</table>
<p>Thanks,</p>
<p>Mark</p>
</body>
</html>

View File

@@ -0,0 +1,20 @@
{
"items": [
{
"valuedate": "01/06/2025",
"counterparty": "Barclays Bank PLC",
"trade_id": 77890112,
"display_ccy": "EUR",
"pv": 8421300,
"breakclause": "Y"
},
{
"valuedate": "01/06/2025",
"counterparty": "Barclays Bank PLC",
"trade_id": 77890113,
"display_ccy": "EUR",
"pv": 22105800,
"breakclause": "Y"
}
]
}

View File

@@ -0,0 +1,39 @@
You are a trade data extraction agent. Your task is to extract structured trade data from sales emails (typically CVA pricing requests) and return the result as JSON.
## Output Schema
Return a JSON object with an "items" array. Each item represents one trade leg and has these fields:
- valuedate (string): The value/observation date in dd/MM/yyyy format. Look for "OB", "Value date", or similar date references in the email.
- counterparty (string): The full legal name of the counterparty as stated in the email prose.
- trade_id (integer): The Murex trade identifier. Each trade leg has a unique Murex ID.
- display_ccy (string): The ISO currency code derived from the email. Map currency symbols: £ → GBP, $ → USD, € → EUR. If stated as an ISO code, use it directly.
- pv (number): The present value as a plain number. Remove commas and currency symbols. Do not round.
- breakclause (string): "Y" if the email mentions a break clause for the trade, "N" otherwise. Default to "N" if not mentioned.
The legal_entity field is NOT included in your output. It is populated later via a counterparty lookup tool.
## Mapping Rules
1. FLATTEN: Each swap leg with a unique Murex trade ID becomes a separate item. A swap with a Coupon Leg (Murex 123) and an APD leg (Murex 456) produces two items.
2. DATE: Parse the value date from context (e.g., "OB 27/11/2025" means valuedate is "27/11/2025"). Always output in dd/MM/yyyy format.
3. COUNTERPARTY: Use the full legal name exactly as written in the email, including any parenthetical former names.
4. CURRENCY: Derive from the PV column header or context. "PV (£)" means GBP. "PV ($)" means USD. "PV (€)" means EUR.
5. PV: Strip formatting (commas, spaces, currency symbols) and output as a plain number.
6. BREAKCLAUSE: Default to "N". Only set to "Y" if the email explicitly mentions a break clause for the trade.
## Output Format
Return ONLY valid JSON. Do not include markdown code fences or explanatory text. Example:
{"items":[{"valuedate":"27/11/2025","counterparty":"Example Corp","trade_id":12345,"display_ccy":"GBP","pv":1234567,"breakclause":"N"}]}
## After Extraction
After producing the initial extraction, use the available validation tools:
- lookup_counterparty: Look up the counterparty name to find matching legal entities.
- validate_trade: Verify each trade ID exists.
- validate_currency: Confirm each currency code is valid.
- validate_schema: Validate the complete extraction result.
If a tool returns multiple candidates (e.g., counterparty lookup), present them to the user as a numbered list and ask which one to use. If a tool indicates an error, attempt to fix the extraction or ask the user for clarification.

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-03

View File

@@ -0,0 +1,33 @@
## Context
CLAUDE.md is a monolithic file containing project identity, tech stack research, and stale GSD workflow sections. OpenSpec is now initialized and provides a structured home for this content as specs.
## Goals / Non-Goals
**Goals:**
- Move project description and constraints into `openspec/specs/project/spec.md`
- Move technology stack research into `openspec/specs/stack/spec.md`
- Populate `openspec/config.yaml` context so AI agents get project context when creating artifacts
- Reduce CLAUDE.md to a slim file that points to OpenSpec for project knowledge
**Non-Goals:**
- Rewriting or editing the migrated content (faithful move, not a rewrite)
- Creating conventions or architecture specs (those are still empty placeholders)
- Changing any application code
## Decisions
### Decision 1: Spec file format
The main specs in `openspec/specs/` will use a prose/reference format (not the WHEN/THEN delta format). The delta specs in the change use WHEN/THEN for requirements tracking, but the actual spec content is the migrated prose — tables, lists, and all.
### Decision 2: CLAUDE.md post-migration content
CLAUDE.md will retain only:
- A one-line project summary
- A pointer to `openspec/specs/` for project knowledge
- Any workflow instructions specific to Claude Code (not project specs)
### Decision 3: config.yaml context
The `context` field in `openspec/config.yaml` will get a brief project summary and tech stack headline, so artifact generation has baseline context without reading full specs.

View File

@@ -0,0 +1,27 @@
## Why
CLAUDE.md currently holds all project knowledge — description, constraints, and a large tech stack research block. With OpenSpec initialized, this content belongs in `openspec/specs/` where it can be managed as proper specs, referenced by changes, and won't conflict with CLAUDE.md's role as a slim workflow/instruction file.
## What Changes
- Extract project description and constraints into a `project` spec
- Extract full technology stack research into a `stack` spec
- Populate `openspec/config.yaml` with project context
- Slim CLAUDE.md down to workflow instructions with pointers to OpenSpec
- Remove stale GSD placeholder sections (conventions, architecture, profile)
## Capabilities
### New Capabilities
- `project`: Project identity, core value statement, and non-negotiable constraints
- `stack`: Technology stack decisions — packages, versions, alternatives, patterns, compatibility, and sources
### Modified Capabilities
<!-- None — no existing specs yet -->
## Impact
- `CLAUDE.md`: Reduced from ~147 lines to a slim pointer file
- `openspec/specs/project/spec.md`: New file with project identity
- `openspec/specs/stack/spec.md`: New file with stack research
- `openspec/config.yaml`: Updated with project context

View File

@@ -0,0 +1,24 @@
## ADDED Requirements
### Requirement: Project identity
The project spec SHALL contain the project name, a description paragraph, and a core value statement that communicates the project's dual purpose: working AI chat interface and Blazor learning journey.
#### Scenario: Project description present
- **WHEN** an AI agent or developer reads the project spec
- **THEN** they find the project name ("Chat Agent WebApp"), a description of what the app does, and the core value statement
### Requirement: Project constraints
The project spec SHALL enumerate all non-negotiable constraints that govern technical decisions across the project.
#### Scenario: Constraints enumerated
- **WHEN** a decision is made about technology, architecture, or approach
- **THEN** the project spec provides the authoritative list of constraints to check against:
- Tech stack: .NET / C# / Blazor WebAssembly
- LLM provider: OpenAI GPT API
- Storage: JSON files on local disk
- Architecture: WASM client + backend API (API key stays server-side)
- Code style: Every Blazor concept introduced MUST have inline comments explaining what it does and why

View File

@@ -0,0 +1,51 @@
## ADDED Requirements
### Requirement: Core technology stack
The stack spec SHALL document the recommended core technologies with version, purpose, and rationale for each.
#### Scenario: Core stack documented
- **WHEN** a developer needs to add or update a dependency
- **THEN** the stack spec provides the authoritative record of: .NET 9 SDK, Blazor WebAssembly Standalone, ASP.NET Core Web API, C# 13, OpenAI SDK 2.9.1, Markdig 1.1.1, MudBlazor 9.2.0, and System.Text.Json
### Requirement: Supporting libraries and tools
The stack spec SHALL document supporting libraries, development tools, and installation notes.
#### Scenario: Supporting libraries referenced
- **WHEN** a developer evaluates adding a new dependency
- **THEN** the stack spec lists supporting libraries with guidance on when to use them (e.g., Microsoft.Extensions.AI — skip for v1)
### Requirement: Alternatives and exclusions
The stack spec SHALL document considered alternatives and explicitly excluded technologies with rationale.
#### Scenario: Alternative considered
- **WHEN** a developer proposes an alternative package or approach
- **THEN** the stack spec provides a record of alternatives already evaluated and why the current choice was made
#### Scenario: Excluded technology referenced
- **WHEN** a developer considers using a technology on the exclusion list
- **THEN** the stack spec explains why it was excluded and what to use instead
### Requirement: Stack patterns
The stack spec SHALL document implementation patterns that govern how stack technologies are used together (streaming, storage, markdown rendering).
#### Scenario: Pattern referenced during implementation
- **WHEN** a developer implements streaming, storage, or markdown rendering
- **THEN** the stack spec provides the canonical pattern to follow
### Requirement: Version compatibility matrix
The stack spec SHALL maintain a compatibility matrix and list of authoritative sources for version decisions.
#### Scenario: Compatibility check
- **WHEN** a package version is being upgraded
- **THEN** the stack spec provides the compatibility matrix to verify cross-package compatibility

View File

@@ -0,0 +1,16 @@
## 1. Create main specs
- [x] 1.1 Create `openspec/specs/project/spec.md` with project description, core value, and constraints from CLAUDE.md
- [x] 1.2 Create `openspec/specs/stack/spec.md` with full technology stack content from CLAUDE.md
## 2. Update config
- [x] 2.1 Populate `openspec/config.yaml` context field with project summary and tech stack headline
## 3. Slim down CLAUDE.md
- [x] 3.1 Replace CLAUDE.md contents with slim pointer file
## 4. Verify
- [x] 4.1 Confirm no content was lost — all substantive information is in specs

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-04

View File

@@ -0,0 +1,45 @@
## Context
No test projects exist. The solution has 3 projects (Client, Api, Shared) under `src/`. Tests need to cover the API controllers and the client's ChatApiClient service, both of which involve HTTP and SSE streaming.
## Goals / Non-Goals
**Goals:**
- Establish test infrastructure (xUnit + Moq)
- Test API controllers using WebApplicationFactory (integration-style)
- Test ChatApiClient using a mock HttpMessageHandler (unit-style)
- All tests runnable via `dotnet test` from solution root
**Non-Goals:**
- Blazor component tests (bUnit) — Chat.razor is UI-heavy, defer to a future phase
- End-to-end browser tests (Playwright/Selenium)
- Testing the upstream Responses API itself
## Decisions
### Decision 1: Test framework — xUnit + Moq
xUnit is the .NET standard. Moq for mocking HttpMessageHandler so we can control HTTP responses in tests without hitting real servers.
### Decision 2: Test project layout
```
tests/
├── ChatAgent.Api.Tests/ → references Api project
└── ChatAgent.Client.Tests/ → references Client project + Shared
```
Both added to the `ChatAgent.sln` solution file under a `tests` solution folder.
### Decision 3: API tests use WebApplicationFactory
`Microsoft.AspNetCore.Mvc.Testing` provides `WebApplicationFactory<Program>` for integration-style tests. For ChatController, we inject a mock `IHttpClientFactory` that returns a handler with canned SSE responses — no real Responses API needed.
### Decision 4: Client tests mock HttpMessageHandler
ChatApiClient takes an HttpClient. Tests create an HttpClient with a custom `DelegatingHandler` that returns canned SSE response streams. This tests the SSE parsing logic in isolation.
## Risks / Trade-offs
- [WebApplicationFactory requires Api's Program to be accessible] → Add `InternalsVisibleTo` or use the `public partial class Program {}` trick in Api's Program.cs
- [SSE stream mocking is verbose] → Create a small helper method that builds SSE response content from a list of events

View File

@@ -0,0 +1,26 @@
## Why
The project has zero test coverage. There are two controllers, a typed HttpClient service, and shared models — all untested. Adding tests now establishes the pattern before the codebase grows, and catches regressions as new phases are added.
## What Changes
- Create an xUnit test project for the API (`ChatAgent.Api.Tests`)
- Create an xUnit test project for the Client services (`ChatAgent.Client.Tests`)
- Add tests for HealthController (GET /api/health)
- Add tests for ChatController (POST /api/chat SSE streaming proxy)
- Add tests for ChatApiClient (GetHealthAsync, SendChatStreamingAsync)
- Add both test projects to the solution
## Capabilities
### New Capabilities
- `test-infrastructure`: Test project setup, test framework choices, and shared test utilities
### Modified Capabilities
<!-- None — adding tests doesn't change existing specs -->
## Impact
- `tests/ChatAgent.Api.Tests/`: New xUnit test project
- `tests/ChatAgent.Client.Tests/`: New xUnit test project
- `ChatAgent.sln`: Add test projects

View File

@@ -0,0 +1,56 @@
## ADDED Requirements
### Requirement: API test project exists
An xUnit test project SHALL exist at `tests/ChatAgent.Api.Tests/` targeting the API project, with xUnit, Moq, and Microsoft.AspNetCore.Mvc.Testing as dependencies.
#### Scenario: API tests run
- **WHEN** `dotnet test` is run from the solution root
- **THEN** API tests are discovered and executed
### Requirement: Client test project exists
An xUnit test project SHALL exist at `tests/ChatAgent.Client.Tests/` targeting the Client services, with xUnit and Moq as dependencies.
#### Scenario: Client tests run
- **WHEN** `dotnet test` is run from the solution root
- **THEN** Client service tests are discovered and executed
### Requirement: HealthController test coverage
Tests SHALL verify that GET /api/health returns HTTP 200 with a valid HealthResponse containing a non-empty Status and a recent Timestamp.
#### Scenario: Health endpoint returns 200
- **WHEN** a GET request is sent to /api/health
- **THEN** the response status is 200 and the body contains `status: "healthy"` and a UTC timestamp
### Requirement: ChatController test coverage
Tests SHALL verify that POST /api/chat returns a streaming SSE response containing text deltas and a [DONE] terminator. Tests SHALL mock the upstream Responses API.
#### Scenario: Chat streams text deltas
- **WHEN** a POST is sent to /api/chat with a valid ChatRequest
- **THEN** the response is `text/event-stream` containing `data: {"text":"..."}` events followed by `data: [DONE]`
#### Scenario: Chat handles upstream error
- **WHEN** the Responses API is unreachable or returns an error
- **THEN** the response contains a `data: {"error":"..."}` event followed by `data: [DONE]`
### Requirement: ChatApiClient test coverage
Tests SHALL verify that SendChatStreamingAsync correctly parses SSE events from the backend into text deltas, handles [DONE], and throws on error events.
#### Scenario: Client parses text deltas
- **WHEN** the backend returns SSE events with text deltas
- **THEN** SendChatStreamingAsync yields each text fragment in order
#### Scenario: Client handles error event
- **WHEN** the backend returns an error SSE event
- **THEN** SendChatStreamingAsync throws HttpRequestException with the error message

View File

@@ -0,0 +1,23 @@
## 1. Test Project Setup
- [x] 1.1 Create xUnit test project at tests/ChatAgent.Api.Tests with xUnit, Moq, Microsoft.AspNetCore.Mvc.Testing
- [x] 1.2 Create xUnit test project at tests/ChatAgent.Client.Tests with xUnit, Moq
- [x] 1.3 Add both test projects to ChatAgent.sln under a tests solution folder
- [x] 1.4 Make Api's Program class accessible to tests (public partial class Program)
## 2. API Tests
- [x] 2.1 Test HealthController: GET /api/health returns 200 with valid HealthResponse
- [x] 2.2 Test ChatController: POST /api/chat streams SSE text deltas from mocked upstream
- [x] 2.3 Test ChatController: POST /api/chat handles upstream error gracefully
## 3. Client Service Tests
- [x] 3.1 Create SSE response helper for building mock SSE streams
- [x] 3.2 Test ChatApiClient.SendChatStreamingAsync: parses text deltas in order
- [x] 3.3 Test ChatApiClient.SendChatStreamingAsync: throws on error event
- [x] 3.4 Test ChatApiClient.GetHealthAsync: returns HealthResponse on success
## 4. Verify
- [x] 4.1 Run dotnet test from solution root — all tests pass

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-03

View File

@@ -0,0 +1,48 @@
## Context
The project has a working Blazor WASM client and ASP.NET Core API with health check connectivity proven. The client currently uses Bootstrap for layout and has template pages (Counter, Weather). No UI component library is installed. This change introduces MudBlazor and builds the first real feature — a chat interface with hardcoded responses.
## Goals / Non-Goals
**Goals:**
- Install and configure MudBlazor as the UI component library
- Build a ChatGPT/Gemini-inspired chat interface
- Establish the message model and UI patterns that future phases will build on
- Keep hardcoded responses so the UI is testable without API wiring
**Non-Goals:**
- OpenAI API integration (future phase)
- Markdown rendering of messages (future phase — Markdig)
- Conversation persistence or history (future phase)
- Multiple conversations / sidebar navigation (future phase)
- Responsive mobile layout optimization
## Decisions
### Decision 1: MudBlazor component choices
**Chat message list**: Use `MudPaper` cards inside a scrollable `div` (or `MudStack`). Each message gets a `MudPaper` with `Elevation="0"` and a background color to distinguish user vs assistant.
**Input area**: `MudTextField` with `Variant="Outlined"` and an `Adornment` send button (icon). This gives a single-line input with integrated send — similar to ChatGPT.
**Layout**: `MudLayout` + `MudAppBar` + `MudMainContent`. No drawer/sidebar yet — that comes when we add conversation management.
**Alternative considered**: Building with raw HTML/CSS. Rejected because MudBlazor is in the tech stack spec and provides the component patterns needed for later phases (dialogs, drawers, lists).
### Decision 2: ChatMessage model location
Place `ChatMessage.cs` in `ChatAgent.Shared` so it's available to both Client and API when API integration comes. Fields: `Role` (string: "user" or "assistant"), `Content` (string), `Timestamp` (DateTime).
### Decision 3: Chat page structure
The Chat.razor component owns the message list (`List<ChatMessage>`) and handles input. No separate service layer yet — the hardcoded response is inline in the component. When AI integration comes, a service will be extracted.
### Decision 4: Template page cleanup
Remove Counter.razor and Weather.razor. Move Home.razor from `/` to `/health` so the health check is still accessible but the chat page takes the root route.
## Risks / Trade-offs
- [MudBlazor WASM bundle size] → Acceptable for a personal tool; AOT is deferred per stack spec
- [No service abstraction for responses] → Intentional; extracting too early adds complexity before the pattern is clear. Will refactor when adding API integration.
- [Removing template pages] → Low risk; they were scaffolding. Health check preserved at `/health`.

View File

@@ -0,0 +1,35 @@
## Why
The project exists to be a working AI chat interface, but there is no chat UI yet — only a health check page. This change builds the foundational chat experience using MudBlazor components, with hardcoded responses so the UI can be developed and tested independently of the OpenAI API integration.
## What Changes
- Install and configure MudBlazor in the Client project (NuGet, CSS/JS, providers, imports)
- Replace the Bootstrap navbar/layout with a MudBlazor layout (MudLayout, MudAppBar, MudMainContent)
- Create a Chat page with a message list and text input, styled after ChatGPT/Gemini
- Add a shared `ChatMessage` model (role + content + timestamp)
- Wire the input to append user messages and reply with a hardcoded bot response
- Remove template pages (Counter, Weather) that are no longer needed
## Capabilities
### New Capabilities
- `chat-ui`: The chat interface — message display, input handling, auto-scroll, and layout
- `mudblazor-setup`: MudBlazor installation, theming, and provider configuration
### Modified Capabilities
<!-- None -->
## Impact
- `src/ChatAgent.Client/ChatAgent.Client.csproj`: Add MudBlazor package
- `src/ChatAgent.Client/wwwroot/index.html`: Add MudBlazor CSS/JS/font links
- `src/ChatAgent.Client/_Imports.razor`: Add MudBlazor using
- `src/ChatAgent.Client/Program.cs`: Add MudBlazor services
- `src/ChatAgent.Client/Layout/MainLayout.razor`: Replace with MudBlazor layout
- `src/ChatAgent.Client/Layout/NavMenu.razor`: Replace or remove Bootstrap nav
- `src/ChatAgent.Client/Pages/Chat.razor`: New chat page (becomes default route)
- `src/ChatAgent.Client/Pages/Home.razor`: Demote from `/` route or keep as `/health`
- `src/ChatAgent.Shared/Models/ChatMessage.cs`: New shared message model
- `src/ChatAgent.Client/Pages/Counter.razor`: Remove
- `src/ChatAgent.Client/Pages/Weather.razor`: Remove

View File

@@ -0,0 +1,66 @@
## ADDED Requirements
### Requirement: Message display
The chat page SHALL display messages in a vertically scrolling list, with each message showing the sender role (user or assistant), the message content, and a visual distinction between user and assistant messages (e.g., alignment, color, or avatar).
#### Scenario: User message displayed
- **WHEN** the user sends a message
- **THEN** the message appears in the message list aligned or styled to indicate it is from the user
#### Scenario: Assistant message displayed
- **WHEN** the assistant responds
- **THEN** the response appears in the message list with distinct styling from user messages (different alignment, color, or avatar)
#### Scenario: Message ordering
- **WHEN** multiple messages exist in the conversation
- **THEN** messages are displayed in chronological order, oldest at top
### Requirement: Message input
The chat page SHALL provide a text input area at the bottom of the page where the user can type and submit messages.
#### Scenario: Submit via button
- **WHEN** the user types text and clicks the send button
- **THEN** the message is added to the conversation and the input is cleared
#### Scenario: Submit via Enter key
- **WHEN** the user types text and presses Enter
- **THEN** the message is submitted (same as clicking send)
#### Scenario: Empty input blocked
- **WHEN** the user attempts to send an empty or whitespace-only message
- **THEN** nothing is sent and no message is added
### Requirement: Hardcoded response
In this phase, the assistant SHALL reply with a hardcoded message to every user input. This stubs the AI integration point for future phases.
#### Scenario: Bot replies to any input
- **WHEN** the user sends any message
- **THEN** the assistant replies with a hardcoded response (e.g., "This is a placeholder response. AI integration coming soon!")
### Requirement: Auto-scroll
The message list SHALL automatically scroll to the newest message when a new message is added.
#### Scenario: New message scrolls into view
- **WHEN** a new message (user or assistant) is added to the conversation
- **THEN** the message list scrolls to the bottom so the new message is visible
### Requirement: Chat page is default route
The chat page SHALL be the default route (`/`) of the application.
#### Scenario: App opens to chat
- **WHEN** the user navigates to the root URL
- **THEN** the chat page is displayed

View File

@@ -0,0 +1,46 @@
## ADDED Requirements
### Requirement: MudBlazor package installed
The Client project SHALL have MudBlazor 9.2.0 installed as a NuGet dependency.
#### Scenario: Package reference present
- **WHEN** the Client project is built
- **THEN** MudBlazor 9.2.0 is resolved as a dependency
### Requirement: MudBlazor services registered
MudBlazor services SHALL be registered in the Client's DI container via `AddMudServices()`.
#### Scenario: Services available
- **WHEN** the application starts
- **THEN** MudBlazor services (snackbar, dialog, etc.) are available for injection
### Requirement: MudBlazor assets loaded
The Client's `index.html` SHALL include MudBlazor CSS, JS, and font references.
#### Scenario: Styles and scripts present
- **WHEN** the application loads in the browser
- **THEN** MudBlazor CSS (`_content/MudBlazor/MudBlazor.min.css`), JS (`_content/MudBlazor/MudBlazor.min.js`), and Material Design Icons font are loaded
### Requirement: MudBlazor layout providers
The app root SHALL include `MudThemeProvider`, `MudPopoverProvider`, and `MudDialogProvider` so MudBlazor components function correctly.
#### Scenario: Providers present
- **WHEN** any MudBlazor component is rendered
- **THEN** it functions correctly because the required providers are in the component tree
### Requirement: MudBlazor layout replaces Bootstrap
The application layout SHALL use MudBlazor layout components (`MudLayout`, `MudAppBar`, `MudMainContent`) instead of the current Bootstrap navbar.
#### Scenario: Layout renders with MudBlazor
- **WHEN** any page is displayed
- **THEN** the page is wrapped in a MudBlazor layout with an app bar showing the application name

View File

@@ -0,0 +1,38 @@
## 1. MudBlazor Setup
- [x] 1.1 Install MudBlazor 9.2.0 NuGet package in ChatAgent.Client
- [x] 1.2 Add MudBlazor CSS, JS, and Material Design Icons font to index.html (remove Bootstrap CSS)
- [x] 1.3 Add `@using MudBlazor` to _Imports.razor
- [x] 1.4 Register MudBlazor services (`AddMudServices()`) in Program.cs
- [x] 1.5 Add MudThemeProvider, MudPopoverProvider, MudDialogProvider to MainLayout.razor
## 2. Layout Migration
- [x] 2.1 Replace MainLayout.razor with MudBlazor layout (MudLayout, MudAppBar, MudMainContent)
- [x] 2.2 Remove NavMenu.razor (Bootstrap navbar no longer needed)
- [x] 2.3 Remove MainLayout.razor.css (MudBlazor handles styling)
## 3. Shared Model
- [x] 3.1 Create ChatMessage.cs in ChatAgent.Shared/Models with Role, Content, Timestamp
## 4. Chat Page
- [x] 4.1 Create Chat.razor at route `/` with message list and input area
- [x] 4.2 Implement message display with MudPaper cards (distinct styling for user vs assistant)
- [x] 4.3 Implement text input with MudTextField and send button adornment
- [x] 4.4 Wire Enter key and send button to submit handler
- [x] 4.5 Block empty/whitespace-only submissions
- [x] 4.6 Add hardcoded assistant response after each user message
- [x] 4.7 Implement auto-scroll to bottom on new messages
## 5. Cleanup
- [x] 5.1 Move Home.razor route from `/` to `/health`
- [x] 5.2 Remove Counter.razor and Weather.razor
- [x] 5.3 Update app.css — remove Bootstrap-specific styles, keep custom styles that still apply
## 6. Verify
- [x] 6.1 Run `dotnet build` on the solution to confirm no errors
- [ ] 6.2 Manually verify: chat page loads at `/`, messages display correctly, hardcoded response works

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-04

View File

@@ -0,0 +1,77 @@
## Context
The chat backend currently proxies requests to a local CLIProxyAPI instance (OpenAI-compatible API at `localhost:8317`) via manual `HttpClient` calls and SSE parsing in `ChatController`. The architecture works for simple chat completion but has no abstraction for tool calling, function invocation, or agentic loops. The goal is to adopt Semantic Kernel as the AI orchestration layer to enable structured extraction with autonomous validation.
## Goals / Non-Goals
**Goals:**
- Replace manual HTTP proxy logic with Semantic Kernel's chat completion service
- Enable tool/function calling via SK plugins
- Implement an agentic extraction loop: extract → validate → retry (up to 3 times) → escalate to user
- Preserve the existing SSE contract so the Blazor client requires no changes
- Maintain inline tutorial comments explaining SK concepts
**Non-Goals:**
- Multi-agent orchestration (future — when Agent Framework reaches GA)
- Changing the Blazor client or `ChatApiClient`
- Adding new UI for structured output display (future change)
- Replacing CLIProxyAPI — SK's OpenAI connector talks to it as-is
- Authentication or multi-user support
## Decisions
### D1: Use SK's OpenAI chat completion connector pointed at CLIProxyAPI
**Choice:** `Microsoft.SemanticKernel.Connectors.OpenAI` with `OpenAIChatCompletionService` configured to use `localhost:8317` as the endpoint.
**Alternatives considered:**
- SK Anthropic connector (talks to Anthropic API directly) — would bypass CLIProxyAPI and lose model-switching flexibility
- Keep manual HttpClient alongside SK — defeats the purpose of the migration
**Rationale:** CLIProxyAPI already provides an OpenAI-compatible interface. SK's OpenAI connector works with any OpenAI-compatible endpoint. No infrastructure change required.
### D2: Register Kernel and plugins in DI via `Program.cs`
**Choice:** Configure `Kernel` in `Program.cs` using `builder.Services.AddKernel()` and register plugins via DI. Inject `Kernel` into `ChatController`.
**Rationale:** Follows ASP.NET Core conventions. The kernel is a singleton service with plugins registered at startup. Controller receives it via constructor injection, consistent with the existing pattern of injecting `IHttpClientFactory` and `IConfiguration`.
### D3: Validation as a native SK plugin function
**Choice:** Create an `ExtractionPlugin` class with `[KernelFunction]` methods: one for validation of extracted fields. The agent auto-invokes this via `ToolCallBehavior.AutoInvokeKernelFunctions`.
**Alternatives considered:**
- Manual tool call loop in controller code — loses SK's built-in retry/function-calling orchestration
- Separate validation service outside SK — requires manual plumbing between LLM and validator
**Rationale:** SK's auto-invocation handles the loop naturally. The LLM sees the validation function as a tool, calls it, reads the result, and decides whether to retry or escalate. This is the core value proposition of adopting SK.
### D4: Iteration cap with human-in-the-loop escalation
**Choice:** Configure `ToolCallBehavior.AutoInvokeKernelFunctions` with `MaximumAutoInvokeAttempts = 3`. If the agent exhausts retries without valid output, it returns a clarification request as a regular chat message to the user.
**Rationale:** The iteration cap prevents runaway loops. The escalation path uses the existing chat UI — the agent simply asks for clarification in natural language, and the user responds in the next message. No special UI needed.
### D5: Preserve SSE contract via streaming kernel invocation
**Choice:** Use `kernel.InvokeStreamingAsync<StreamingChatMessageContent>()` (or `IChatCompletionService.GetStreamingChatMessageContentsAsync()`) and re-emit tokens as the same SSE format the client expects: `data: {"text":"..."}\n\n` and `data: [DONE]\n\n`.
**Rationale:** The Blazor client's `ChatApiClient` parses this exact format. By keeping the SSE contract identical, the entire client codebase remains untouched.
### D6: Predefined extraction schema as a strongly-typed C# class
**Choice:** Define an `ExtractedFields` record/class in `ChatAgent.Shared.Models` with the fixed set of known fields. Validation logic checks for required fields and type correctness.
**Rationale:** Single output type with fixed keys. A strongly-typed class gives compile-time safety, works with `System.Text.Json` serialization, and can carry data annotations for validation rules.
## Risks / Trade-offs
- **[SK OpenAI connector compatibility with CLIProxyAPI]** → CLIProxyAPI aims for OpenAI API parity but may have edge cases with tool calling responses. Mitigation: test tool calling end-to-end early; fall back to direct Anthropic connector if needed.
- **[Streaming + tool calling interaction]** → When the agent calls a tool mid-stream, the streaming behavior may differ from pure chat completion. Mitigation: handle tool call chunks in the SSE bridge; may need to buffer during tool execution and resume streaming after.
- **[SK version churn]** → Semantic Kernel is actively developed; APIs may evolve. Mitigation: pin to a specific stable version, document the version in stack spec.
- **[Tutorial complexity increase]** → SK adds abstractions (kernel, plugins, functions) that need explaining. Mitigation: maintain inline comments for every SK concept, consistent with project convention.
## Open Questions
- What are the exact field names and types for `ExtractedFields`? (Need user input for the real schema — can use a placeholder for initial implementation.)
- Should tool call status ("Validating output...") be surfaced to the client as a distinct SSE event type, or just as regular text tokens? (Current design: regular text, revisit in a future change if needed.)

View File

@@ -0,0 +1,28 @@
## Why
The chat backend currently proxies requests to an OpenAI-compatible API (CLIProxyAPI) via manual HttpClient calls and SSE parsing. As the agent evolves toward structured extraction with tool calling and autonomous validation loops, this manual plumbing becomes a liability. Semantic Kernel provides a production-ready abstraction for chat completion, tool/function calling, and auto-invocation — letting us focus on agent behavior rather than HTTP mechanics. Adopting it now establishes the foundation for the agentic workflow (natural language → structured extraction → tool-based validation → human-in-the-loop clarification).
## What Changes
- Replace manual HttpClient + SSE proxy in `ChatController` with Semantic Kernel's `OpenAIChatCompletionService` pointed at the existing CLIProxyAPI proxy
- Add a validation plugin that the agent can call as a tool to validate extracted key-value output against a predefined schema
- Introduce an agentic loop: the kernel autonomously retries extraction up to 23 times on validation failure, then escalates to the user for clarification
- Keep the existing SSE contract to the Blazor client unchanged — `ChatApiClient` and `Chat.razor` are not modified
- **BREAKING**: `ChatController` internals are rewritten; the manual Responses API proxy logic is removed entirely
## Capabilities
### New Capabilities
- `agent-extraction`: Defines the structured field extraction behavior — predefined keys, validation rules, autonomous retry loop, and human-in-the-loop escalation
- `semantic-kernel-integration`: Defines how Semantic Kernel is configured, registered, and wired into the API — kernel setup, OpenAI connector config, plugin registration
### Modified Capabilities
- `chat-streaming`: The streaming requirement changes from "proxy SSE from upstream API" to "stream Semantic Kernel chat completion responses as SSE" — same client contract, different server implementation
## Impact
- **ChatAgent.Api**: New NuGet dependencies (`Microsoft.SemanticKernel`), `Program.cs` service registration changes, `ChatController` rewritten
- **ChatAgent.Api.Tests**: Existing `ChatControllerTests` need updating to mock Semantic Kernel services instead of upstream HTTP calls
- **Dependencies**: Adds `Microsoft.SemanticKernel` and `Microsoft.SemanticKernel.Connectors.OpenAI` packages
- **Infrastructure**: No change — still talks to CLIProxyAPI at `localhost:8317`
- **Client**: No change — SSE contract preserved

View File

@@ -0,0 +1,66 @@
## ADDED Requirements
### Requirement: Structured field extraction from natural language
The agent SHALL extract a predefined set of key-value pairs from user-provided natural language text (e.g., email content) and return them as a structured JSON object.
#### Scenario: All fields extracted successfully
- **WHEN** the user sends a message containing natural language with all required information
- **THEN** the agent returns a JSON object with all predefined fields populated from the text
#### Scenario: Partial extraction
- **WHEN** the user sends a message that contains some but not all required fields
- **THEN** the agent extracts available fields and leaves missing fields as null
### Requirement: Predefined extraction schema
The system SHALL define a fixed set of known field names and types as a strongly-typed C# class. All extraction output MUST conform to this schema.
#### Scenario: Output conforms to schema
- **WHEN** the agent produces extracted fields
- **THEN** every key in the output matches a field defined in the schema and values match expected types
### Requirement: Autonomous validation via tool calling
The agent SHALL validate extracted fields by calling a validation tool function. The validation tool checks that all required fields are present and correctly typed.
#### Scenario: Validation passes
- **WHEN** the agent calls the validation tool with a complete and correct extraction
- **THEN** the tool returns a success result and the agent returns the final output to the user
#### Scenario: Validation fails with fixable errors
- **WHEN** the validation tool returns errors for missing or malformed fields
- **THEN** the agent re-reads the source text and attempts to fix the extraction without user intervention
### Requirement: Autonomous retry with iteration cap
The agent SHALL retry extraction autonomously up to 3 times when validation fails. After exhausting retries, the agent MUST escalate to the user.
#### Scenario: Agent retries and succeeds
- **WHEN** validation fails on the first attempt but the error is recoverable
- **THEN** the agent retries extraction and calls validation again, up to 3 total attempts
#### Scenario: Agent exhausts retries and escalates
- **WHEN** validation fails after 3 attempts
- **THEN** the agent sends a natural language message to the user identifying the specific fields it could not resolve and asking for clarification
### Requirement: Human-in-the-loop clarification
When the agent escalates to the user, the user SHALL be able to provide the missing information in natural language, and the agent SHALL incorporate the clarification and re-attempt extraction.
#### Scenario: User provides clarification
- **WHEN** the agent asks for clarification about missing fields and the user responds
- **THEN** the agent incorporates the user's response into the conversation context and produces an updated extraction
#### Scenario: Clarification via normal chat
- **WHEN** the agent escalates for clarification
- **THEN** the clarification request appears as a regular assistant message in the chat UI, and the user responds via the normal chat input

View File

@@ -0,0 +1,42 @@
## MODIFIED Requirements
### Requirement: Chat endpoint proxies to Responses API
The API backend SHALL expose `POST /api/chat` that accepts a list of messages and processes them using a Semantic Kernel chat completion service. The kernel is configured with an OpenAI connector pointed at the existing CLIProxyAPI proxy.
#### Scenario: Successful chat request
- **WHEN** the client sends a POST to `/api/chat` with a message list
- **THEN** the API processes the messages through the Semantic Kernel and returns the response
### Requirement: Streaming response delivery
The API backend SHALL stream the Semantic Kernel's chat completion response back to the WASM client as `text/event-stream`, forwarding text content so the client can render tokens incrementally. The SSE event format MUST remain `data: {"text":"..."}\n\n` for text deltas and `data: [DONE]\n\n` for completion.
#### Scenario: Tokens stream to client
- **WHEN** the Semantic Kernel emits streaming chat message content
- **THEN** the backend forwards each content chunk as an SSE event to the client containing the text fragment
#### Scenario: Stream completes
- **WHEN** the Semantic Kernel streaming response completes
- **THEN** the backend signals stream completion to the client with `data: [DONE]\n\n`
### Requirement: Configurable proxy target
The CLIProxyAPI base URL and model name SHALL be configurable via `appsettings.json` in the API project, not hardcoded. These values are used to configure the Semantic Kernel OpenAI connector.
#### Scenario: Configuration read at startup
- **WHEN** the API starts
- **THEN** it reads `ResponsesApi:BaseUrl` and `ResponsesApi:Model` from configuration to configure the Semantic Kernel
### Requirement: Error propagation
If the LLM service returns an error or is unreachable, the API backend SHALL return an error SSE event and the client SHALL display the error to the user.
#### Scenario: LLM service unreachable
- **WHEN** the CLIProxyAPI proxy is not running
- **THEN** the client displays an error message instead of an assistant response

View File

@@ -0,0 +1,42 @@
## ADDED Requirements
### Requirement: Semantic Kernel service registration
The API backend SHALL register a Semantic Kernel `Kernel` instance in the ASP.NET Core DI container at startup, configured with an OpenAI chat completion connector.
#### Scenario: Kernel registered at startup
- **WHEN** the API application starts
- **THEN** a `Kernel` instance is available for injection into controllers
### Requirement: OpenAI connector targets CLIProxyAPI proxy
The Semantic Kernel OpenAI chat completion service SHALL be configured to use the existing CLIProxyAPI proxy endpoint as its base URL, reading the URL and model name from `appsettings.json`.
#### Scenario: Connector uses configured endpoint
- **WHEN** the kernel makes a chat completion request
- **THEN** it sends the request to the URL specified in `ResponsesApi:BaseUrl` configuration
#### Scenario: Model from configuration
- **WHEN** the kernel makes a chat completion request
- **THEN** it uses the model name specified in `ResponsesApi:Model` configuration
### Requirement: Plugin registration
The API backend SHALL register extraction and validation plugins with the Kernel so they are available as tools for the LLM to invoke.
#### Scenario: Plugins available as tools
- **WHEN** the kernel is constructed
- **THEN** all registered plugin functions appear in the tool list sent to the LLM
### Requirement: Auto function calling
The Kernel SHALL be configured with automatic function calling enabled, allowing the LLM to invoke registered plugin functions without manual dispatch code.
#### Scenario: LLM invokes tool automatically
- **WHEN** the LLM decides to call a registered function during chat completion
- **THEN** the kernel automatically executes the function and returns the result to the LLM

View File

@@ -0,0 +1,40 @@
## 1. Add Semantic Kernel Dependencies
- [x] 1.1 Add `Microsoft.SemanticKernel` and `Microsoft.SemanticKernel.Connectors.OpenAI` NuGet packages to `ChatAgent.Api`
- [x] 1.2 Remove the `OpenAI` SDK package if no longer needed after migration
## 2. Define Extraction Schema
- [x] 2.1 Create `ExtractedFields` class in `ChatAgent.Shared/Models/` with the predefined set of key-value fields (placeholder fields until real schema is provided)
- [x] 2.2 Create `ValidationResult` class in `ChatAgent.Shared/Models/` with `IsValid`, `Errors` properties
## 3. Create Extraction Plugin
- [x] 3.1 Create `ExtractionPlugin` class in `ChatAgent.Api/Plugins/` with a `[KernelFunction]` validation method that checks `ExtractedFields` for required fields and type correctness
- [x] 3.2 Add inline tutorial comments explaining SK plugin concepts (`[KernelFunction]`, `[Description]`, auto-invocation)
## 4. Wire Semantic Kernel in Program.cs
- [x] 4.1 Register `OpenAIChatCompletionService` in DI using `ResponsesApi:BaseUrl` and `ResponsesApi:Model` from config
- [x] 4.2 Register `Kernel` with `AddKernel()` and import `ExtractionPlugin`
- [x] 4.3 Add inline tutorial comments explaining kernel setup, connectors, and plugin registration
## 5. Rewrite ChatController
- [x] 5.1 Replace `IHttpClientFactory` and `IConfiguration` injection with `Kernel` injection
- [x] 5.2 Replace manual HTTP proxy logic with `IChatCompletionService.GetStreamingChatMessageContentsAsync()` using the conversation history from the request
- [x] 5.3 Configure `OpenAIPromptExecutionSettings` with `FunctionChoiceBehavior.Auto()` and `autoInvokeMaxCallCount = 3`
- [x] 5.4 Re-emit streaming content as the existing SSE format (`data: {"text":"..."}\n\n` and `data: [DONE]\n\n`)
- [x] 5.5 Add inline tutorial comments explaining streaming chat completion, execution settings, and tool call behavior
## 6. Update Tests
- [x] 6.1 Update `ChatControllerTests` to mock `IChatCompletionService` instead of upstream HTTP calls
- [x] 6.2 Add tests for the validation plugin (`ExtractionPlugin` returns correct pass/fail results)
- [x] 6.3 Add a test verifying the agent escalates to the user after max retries
## 7. Verify
- [x] 7.1 Run `dotnet build` to confirm no errors
- [x] 7.2 Run `dotnet test` to confirm all tests pass
- [ ] 7.3 Manual smoke test: send a chat message and verify streaming still works end-to-end through SK

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-04

View File

@@ -0,0 +1,30 @@
## Context
Chat.razor currently constructs a `ChatRequest` with only the latest user message (line 161-164). The backend and Responses API already support multi-message input — no server changes needed. This is purely a client-side change.
## Goals / Non-Goals
**Goals:**
- Send full conversation history with each request for multi-turn context
- Add a "New Chat" button to reset the session
**Non-Goals:**
- Persistent conversation storage (explicitly deferred)
- Multiple conversation tabs/sidebar
- Token limit management or history truncation (small conversations for now)
## Decisions
### Decision 1: Send full _messages list minus the empty placeholder
When building the `ChatRequest`, include all messages from `_messages` except the last one (which is the empty assistant placeholder waiting for streaming). This gives the AI the full conversation context.
### Decision 2: New Chat button in the AppBar
Place a "New Chat" icon button in the `MudAppBar` (in MainLayout.razor or Chat.razor). Clicking it clears `_messages` and resets to the empty state. Disabled during streaming.
Alternative considered: putting it in the input area. Rejected — the AppBar is more natural and matches ChatGPT/Gemini placement.
## Risks / Trade-offs
- [No token limit management] → For long conversations, the full history could exceed the model's context window. Acceptable for now; truncation can be added later.

View File

@@ -0,0 +1,21 @@
## Why
Each chat request currently sends only the latest user message, so the AI has no memory of previous exchanges within the same session. Users expect the assistant to remember context from earlier in the conversation, like ChatGPT/Gemini do.
## What Changes
- Send the full conversation history (all prior user and assistant messages) with each API request instead of just the latest user message
- The backend already forwards all messages in `ChatRequest.Messages` to the Responses API — no backend changes needed
- Add a "New Chat" button to clear the conversation and start fresh
## Capabilities
### New Capabilities
<!-- None -->
### Modified Capabilities
- `chat-ui`: Send full message history with each request; add a "New Chat" button to reset the conversation
## Impact
- `src/ChatAgent.Client/Pages/Chat.razor`: Change request construction to include full history; add new chat button

View File

@@ -0,0 +1,31 @@
## MODIFIED Requirements
### Requirement: Streaming AI response
The assistant SHALL reply with a real AI response streamed from the backend API, using the full conversation history as context. Tokens appear incrementally as they arrive.
#### Scenario: Bot replies with streamed AI response
- **WHEN** the user sends any message
- **THEN** the assistant message appears and grows token by token as the stream delivers text
#### Scenario: Full history sent with each request
- **WHEN** the user sends a message after prior exchanges
- **THEN** all previous user and assistant messages are included in the API request so the AI has conversational context
## ADDED Requirements
### Requirement: New chat button
The chat page SHALL provide a button to clear the current conversation and start a new one.
#### Scenario: User starts a new chat
- **WHEN** the user clicks the "New Chat" button
- **THEN** all messages are cleared and the empty state is shown
#### Scenario: New chat button disabled during streaming
- **WHEN** the assistant is currently streaming a response
- **THEN** the "New Chat" button is disabled

View File

@@ -0,0 +1,13 @@
## 1. Multi-turn History
- [x] 1.1 Change ChatRequest construction in Chat.razor to send all prior messages (excluding the empty assistant placeholder) instead of just the latest user message
## 2. New Chat Button
- [x] 2.1 Add a "New Chat" icon button to the AppBar or chat page that clears _messages
- [x] 2.2 Disable the "New Chat" button while streaming is in progress
## 3. Verify
- [x] 3.1 Run dotnet build to confirm no errors
- [x] 3.2 Run dotnet test to confirm existing tests still pass

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-04

View File

@@ -0,0 +1,66 @@
## Context
The chat UI has a hardcoded response stub. A local OpenAI-compatible proxy at `localhost:8317` serves the Responses API (`POST /v1/responses`) with Claude models. The existing architecture has a WASM client calling the API backend — we add a new endpoint that proxies to the Responses API and streams tokens back.
The Responses API streaming format uses SSE with events like `response.output_text.delta` carrying a `delta` field with text fragments.
## Goals / Non-Goals
**Goals:**
- Wire real AI responses through the existing client → backend → proxy chain
- Stream tokens to the UI for responsive feel
- Keep the proxy URL and model configurable (server-side only)
- Show a thinking indicator while waiting for first token
**Non-Goals:**
- Conversation history / multi-turn context (future phase)
- Model selection UI (future phase)
- Retry logic or rate limiting
- Markdown rendering of responses (future phase — Markdig)
## Decisions
### Decision 1: Backend proxies the Responses API
The WASM client cannot call `localhost:8317` directly (different origin, and we keep external service URLs server-side). The API backend gets a new `ChatController` that:
1. Receives messages from the client
2. Forwards them to `POST /v1/responses` with `"stream": true`
3. Reads the SSE stream, extracts `response.output_text.delta` events
4. Re-emits the text deltas as simple SSE events to the client
**Format for client SSE**: `data: {"text": "<delta>"}\n\n` for each token, and `data: [DONE]\n\n` at the end. This is simpler than forwarding the full Responses API event structure.
**Alternative considered**: Having the client call `localhost:8317` directly via CORS. Rejected — breaks the architecture constraint of keeping external URLs server-side.
### Decision 2: Client-side streaming with SetBrowserResponseStreamingEnabled
Per the stack spec, the client uses:
- `SetBrowserResponseStreamingEnabled(true)` on the `HttpRequestMessage`
- `HttpCompletionOption.ResponseHeadersRead` to start reading before the full response arrives
- Line-by-line iteration of the response stream
This avoids any JavaScript interop for streaming.
### Decision 3: Simple DTOs in Shared project
Add `ChatRequest` (list of messages) and keep the existing `ChatMessage` model. The SSE parsing happens in `ChatApiClient` — no DTO needed for individual stream events since they're parsed inline.
### Decision 4: Configuration via appsettings.json
The API's `appsettings.json` gets:
```json
{
"ResponsesApi": {
"BaseUrl": "http://localhost:8317",
"Model": "claude-sonnet-4-6"
}
}
```
This is the API project's appsettings (server-side, not exposed to the browser).
## Risks / Trade-offs
- [Proxy adds latency] → Minimal for localhost; acceptable tradeoff for keeping URLs server-side
- [No conversation history] → Intentional; each request is single-turn for now. Multi-turn comes in a future phase.
- [No retry on stream failure] → If the stream breaks mid-response, the partial text stays visible and an error is shown. Good enough for phase 1.

View File

@@ -0,0 +1,30 @@
## Why
The chat UI currently returns hardcoded responses. A local OpenAI-compatible proxy is running at `localhost:8317` that exposes the Responses API (`POST /v1/responses`) backed by Anthropic Claude models. This change wires the chat to produce real AI responses via streaming, replacing the hardcoded stub.
## What Changes
- Add a chat endpoint to the API backend that proxies requests to the local Responses API
- Stream tokens from the Responses API back to the WASM client as SSE
- Update ChatApiClient with a streaming chat method
- Replace the hardcoded response in Chat.razor with live streaming from the API
- Add a "thinking" indicator while the assistant is responding
- Disable input during streaming to prevent overlapping requests
## Capabilities
### New Capabilities
- `chat-streaming`: Streaming AI responses from the Responses API proxy through the backend to the WASM client
### Modified Capabilities
- `chat-ui`: Replace hardcoded response with streaming AI response, add typing indicator, disable input during streaming
## Impact
- `src/ChatAgent.Api/ChatAgent.Api.csproj`: Add no new packages (uses built-in HttpClient)
- `src/ChatAgent.Api/Controllers/ChatController.cs`: New controller proxying to Responses API
- `src/ChatAgent.Api/Program.cs`: Register HttpClient for the proxy, add configuration
- `src/ChatAgent.Api/appsettings.json`: New — configure Responses API base URL and model
- `src/ChatAgent.Client/Services/ChatApiClient.cs`: Add streaming chat method
- `src/ChatAgent.Client/Pages/Chat.razor`: Replace hardcoded response with streaming call
- `src/ChatAgent.Shared/Models/`: New request/response DTOs for the chat endpoint

View File

@@ -0,0 +1,51 @@
## ADDED Requirements
### Requirement: Chat endpoint proxies to Responses API
The API backend SHALL expose `POST /api/chat` that accepts a list of messages and proxies the request to the local Responses API at a configurable base URL using the `POST /v1/responses` endpoint.
#### Scenario: Successful proxy request
- **WHEN** the client sends a POST to `/api/chat` with a message list
- **THEN** the API forwards the messages to the Responses API with the configured model and returns the response
### Requirement: Streaming response delivery
The API backend SHALL stream the Responses API's SSE events back to the WASM client as `text/event-stream`, forwarding `response.output_text.delta` events so the client can render tokens incrementally.
#### Scenario: Tokens stream to client
- **WHEN** the Responses API emits `response.output_text.delta` events
- **THEN** the backend forwards each delta as an SSE event to the client containing the text fragment
#### Scenario: Stream completes
- **WHEN** the Responses API emits `response.completed`
- **THEN** the backend signals stream completion to the client
### Requirement: Configurable proxy target
The Responses API base URL and model name SHALL be configurable via `appsettings.json` in the API project, not hardcoded.
#### Scenario: Configuration read at startup
- **WHEN** the API starts
- **THEN** it reads `ResponsesApi:BaseUrl` and `ResponsesApi:Model` from configuration
### Requirement: Client streams from backend
The WASM client SHALL call `POST /api/chat` with `SetBrowserResponseStreamingEnabled(true)` and `HttpCompletionOption.ResponseHeadersRead`, then iterate the SSE stream to update the UI token by token.
#### Scenario: Client reads streaming response
- **WHEN** the client sends a chat request
- **THEN** it reads the response stream incrementally and appends each text delta to the assistant message in real time
### Requirement: Error propagation
If the Responses API returns an error or is unreachable, the API backend SHALL return an appropriate HTTP error status and the client SHALL display the error to the user.
#### Scenario: Proxy unreachable
- **WHEN** the Responses API is not running
- **THEN** the client displays an error message instead of an assistant response

View File

@@ -0,0 +1,50 @@
## MODIFIED Requirements
### Requirement: Hardcoded response
The assistant SHALL reply with a real AI response streamed from the backend API, replacing the previous hardcoded stub. Tokens appear incrementally as they arrive.
#### Scenario: Bot replies with streamed AI response
- **WHEN** the user sends any message
- **THEN** the assistant message appears and grows token by token as the stream delivers text
### Requirement: Message input
The chat page SHALL provide a text input area at the bottom of the page where the user can type and submit messages.
#### Scenario: Submit via button
- **WHEN** the user types text and clicks the send button
- **THEN** the message is added to the conversation and the input is cleared
#### Scenario: Submit via Enter key
- **WHEN** the user types text and presses Enter
- **THEN** the message is submitted (same as clicking send)
#### Scenario: Empty input blocked
- **WHEN** the user attempts to send an empty or whitespace-only message
- **THEN** nothing is sent and no message is added
#### Scenario: Input disabled during streaming
- **WHEN** the assistant is currently streaming a response
- **THEN** the input field and send button are disabled until streaming completes
## ADDED Requirements
### Requirement: Thinking indicator
The chat page SHALL show a visual indicator while waiting for the first token from the assistant.
#### Scenario: Indicator shown during wait
- **WHEN** the user sends a message and the assistant has not yet started streaming
- **THEN** a thinking indicator (e.g., animated dots) is shown in the assistant message area
#### Scenario: Indicator replaced by content
- **WHEN** the first token arrives from the stream
- **THEN** the thinking indicator is replaced by the streamed text

View File

@@ -0,0 +1,29 @@
## 1. Shared Models
- [x] 1.1 Create ChatRequest.cs in ChatAgent.Shared/Models with a Messages list property
## 2. API Backend
- [x] 2.1 Add appsettings.json to ChatAgent.Api with ResponsesApi:BaseUrl and ResponsesApi:Model
- [x] 2.2 Register an HttpClient for the Responses API proxy in Api Program.cs
- [x] 2.3 Create ChatController with POST /api/chat that proxies to the Responses API with streaming
- [x] 2.4 Parse Responses API SSE stream, extract response.output_text.delta events, re-emit as simplified SSE to client
## 3. Client Streaming
- [x] 3.1 Add a streaming SendChatAsync method to ChatApiClient that uses SetBrowserResponseStreamingEnabled and HttpCompletionOption.ResponseHeadersRead
- [x] 3.2 Parse the simplified SSE stream line-by-line, yielding text deltas
## 4. Chat Page Updates
- [x] 4.1 Replace hardcoded response in Chat.razor with a call to ChatApiClient.SendChatAsync
- [x] 4.2 Append tokens to the assistant message incrementally with StateHasChanged after each delta
- [x] 4.3 Add a thinking indicator shown until the first token arrives
- [x] 4.4 Disable input field and send button while streaming is in progress
- [x] 4.5 Handle errors — display error message if API call fails
- [x] 4.6 Auto-scroll during streaming (not just at the end)
## 5. Verify
- [x] 5.1 Run dotnet build to confirm no errors
- [ ] 5.2 Manually verify: send a message, see streaming response from Claude

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-05

View File

@@ -0,0 +1,57 @@
## Context
Assistant messages from the LLM contain markdown formatting (bold, code blocks, lists, headings) but are currently rendered as plain text via `<MudText>@message.Content</MudText>`. Markdig 1.1.1 is already in the stack spec but not yet wired up. The Blazor WASM client needs a rendering pipeline that converts markdown to HTML and displays it safely inside chat bubbles.
## Goals / Non-Goals
**Goals:**
- Render assistant message markdown as formatted HTML (headings, bold, italic, code, lists, tables, links)
- Sanitize HTML output to prevent XSS from LLM-generated content
- Style rendered elements to look good inside MudBlazor chat bubbles
- Maintain streaming performance — re-render as tokens arrive without flicker
**Non-Goals:**
- Rendering user messages as markdown (they stay plain text)
- Syntax highlighting for code blocks (future enhancement)
- LaTeX/math rendering
- Image rendering from markdown
## Decisions
### D1: Use Markdig for markdown-to-HTML conversion
Markdig is already in the stack spec. It's the standard .NET markdown library, runs in WASM, and supports GFM (GitHub Flavored Markdown) extensions out of the box.
**Alternative**: Custom regex-based parsing — rejected, fragile and incomplete.
### D2: Render via `MarkupString` in Blazor
Blazor's `MarkupString` struct renders raw HTML in the component tree. We wrap the Markdig HTML output in `MarkupString` to display it. This replaces `@message.Content` with `@((MarkupString)renderedHtml)` for assistant messages only.
**Alternative**: Use a third-party Blazor markdown component — rejected, adds a dependency when Markdig + MarkupString achieves the same result with less coupling.
### D3: HTML sanitization via allowlist approach
LLM output is untrusted. After Markdig converts markdown to HTML, we strip any tags/attributes not on an allowlist. Allowed: `p, h1-h6, strong, em, code, pre, ul, ol, li, a (href only), table, thead, tbody, tr, th, td, br, blockquote`. This prevents script injection without a heavy sanitizer dependency.
**Alternative**: Use a NuGet sanitizer package (e.g., HtmlSanitizer) — viable but adds a dependency for a simple allowlist. If the allowlist proves insufficient, revisit.
### D4: Create a MarkdownService for the conversion pipeline
A `MarkdownService` class encapsulates the Markdig pipeline configuration, HTML sanitization, and caching. Registered as singleton in DI. This keeps the rendering logic out of the Razor component and makes it testable.
### D5: Incremental rendering during streaming
During streaming, the assistant message content grows token by token. Each time `StateHasChanged()` is called, the full content string is re-converted through Markdig. This is acceptable because:
- Markdig is fast (sub-millisecond for typical message sizes)
- Messages rarely exceed a few KB
- No DOM diffing overhead — Blazor handles this efficiently
If performance becomes an issue, we can cache the last-known-good HTML prefix and only re-render the delta.
## Risks / Trade-offs
- **[XSS from LLM output]** → Mitigated by HTML sanitization allowlist. The allowlist is conservative — only structural tags, no script/style/event handlers.
- **[Streaming flicker]** → Low risk. Blazor's diffing minimizes DOM updates. If observed, add debouncing to StateHasChanged calls.
- **[Markdig WASM size]** → Markdig adds ~200KB to the WASM bundle. Already accepted in stack spec.
- **[Incomplete markdown support]** → Markdig supports GFM by default. Edge cases (nested tables, complex HTML blocks) may render imperfectly but are rare in LLM output.

View File

@@ -0,0 +1,24 @@
## Why
Assistant messages currently render as plain text — markdown formatting (bold, code blocks, lists, headings) from the LLM appears as raw characters. With Semantic Kernel and tool calling now in place, responses are increasingly structured and harder to read without proper rendering. Markdig 1.1.1 is already in the stack but not wired up.
## What Changes
- Render assistant message content as HTML by converting markdown via Markdig
- Sanitize rendered HTML to prevent XSS (the LLM output is untrusted content)
- Style rendered markdown elements (code blocks, lists, tables) to fit the chat bubble aesthetic
- Keep user messages as plain text (they are short inputs, not markdown)
## Capabilities
### New Capabilities
- `rich-text-display`: Markdown-to-HTML rendering pipeline for assistant messages, including sanitization and styling
### Modified Capabilities
- `chat-ui`: Assistant message display changes from plain text to rendered markdown
## Impact
- ChatAgent.Client: Chat.razor message rendering, new markdown service, CSS additions
- Dependencies: Markdig already in stack spec; may need an HTML sanitizer package
- No backend changes — this is purely client-side rendering

View File

@@ -0,0 +1,20 @@
## MODIFIED Requirements
### Requirement: Message display
The chat page SHALL display messages in a vertically scrolling list, with each message showing the sender role (user or assistant), the message content, and a visual distinction between user and assistant messages (e.g., alignment, color, or avatar). Assistant messages SHALL render content as formatted HTML converted from markdown; user messages SHALL display as plain text.
#### Scenario: User message displayed
- **WHEN** the user sends a message
- **THEN** the message appears in the message list aligned or styled to indicate it is from the user, rendered as plain text
#### Scenario: Assistant message displayed
- **WHEN** the assistant responds
- **THEN** the response appears in the message list with distinct styling from user messages, with markdown content rendered as formatted HTML
#### Scenario: Message ordering
- **WHEN** multiple messages exist in the conversation
- **THEN** messages are displayed in chronological order, oldest at top

View File

@@ -0,0 +1,91 @@
## ADDED Requirements
### Requirement: Markdown rendering for assistant messages
The system SHALL convert assistant message content from markdown to formatted HTML using Markdig, displaying headings, bold, italic, code blocks, lists, tables, links, and blockquotes with proper visual formatting.
#### Scenario: Markdown bold and italic rendered
- **WHEN** an assistant message contains `**bold**` or `*italic*` text
- **THEN** the text is displayed with bold or italic formatting respectively
#### Scenario: Code block rendered
- **WHEN** an assistant message contains a fenced code block (triple backticks)
- **THEN** the code is displayed in a monospace font within a visually distinct block
#### Scenario: Inline code rendered
- **WHEN** an assistant message contains inline code (single backticks)
- **THEN** the code is displayed in a monospace font with a subtle background
#### Scenario: List rendered
- **WHEN** an assistant message contains a markdown list (ordered or unordered)
- **THEN** the list is displayed with proper indentation and bullet/number markers
#### Scenario: Heading rendered
- **WHEN** an assistant message contains markdown headings (# through ######)
- **THEN** the headings are displayed with appropriate size and weight
#### Scenario: Link rendered
- **WHEN** an assistant message contains a markdown link `[text](url)`
- **THEN** the link is displayed as a clickable hyperlink opening in a new tab
#### Scenario: Table rendered
- **WHEN** an assistant message contains a markdown table
- **THEN** the table is displayed with borders, header row styling, and proper alignment
### Requirement: HTML sanitization
The system SHALL sanitize all HTML output from the markdown renderer to prevent cross-site scripting (XSS) attacks from LLM-generated content.
#### Scenario: Script tags stripped
- **WHEN** assistant message content contains `<script>` tags
- **THEN** the script tags and their content are removed from the rendered output
#### Scenario: Event handlers stripped
- **WHEN** assistant message content contains HTML attributes like `onclick` or `onerror`
- **THEN** the attributes are removed from the rendered output
#### Scenario: Safe tags preserved
- **WHEN** assistant message content contains allowed structural HTML (p, strong, em, code, pre, ul, ol, li, a, table elements, blockquote, br, h1-h6)
- **THEN** the tags are preserved in the rendered output
### Requirement: Markdown styling within chat bubbles
The system SHALL style rendered markdown elements to be visually consistent with the MudBlazor chat bubble theme.
#### Scenario: Code block styled in bubble
- **WHEN** a code block is rendered inside an assistant chat bubble
- **THEN** it has a distinct background color, padding, border-radius, and does not overflow the bubble width (horizontal scroll if needed)
#### Scenario: Paragraph spacing in bubble
- **WHEN** multiple paragraphs are rendered inside an assistant chat bubble
- **THEN** paragraphs have appropriate spacing without excessive gaps
### Requirement: Streaming compatibility
The system SHALL re-render markdown as new tokens arrive during streaming without visual glitches.
#### Scenario: Partial markdown rendered during streaming
- **WHEN** tokens are arriving and the current content contains incomplete markdown (e.g., a code block not yet closed)
- **THEN** the content is rendered with best-effort formatting and updates smoothly as more tokens complete the markdown structure
### Requirement: User messages remain plain text
The system SHALL continue to render user messages as plain text without markdown processing.
#### Scenario: User message not processed as markdown
- **WHEN** a user message contains markdown syntax like `**bold**`
- **THEN** the raw text `**bold**` is displayed, not formatted bold text

View File

@@ -0,0 +1,26 @@
## 1. Markdown Service
- [x] 1.1 Add Markdig NuGet package to ChatAgent.Client project
- [x] 1.2 Create MarkdownService class with a ConfigureMarkdigPipeline method using AdvancedExtensions (GFM tables, task lists, pipe tables)
- [x] 1.3 Add ConvertToHtml method that takes a markdown string and returns sanitized HTML
- [x] 1.4 Implement HTML sanitization via tag/attribute allowlist (p, h1-h6, strong, em, code, pre, ul, ol, li, a[href], table, thead, tbody, tr, th, td, br, blockquote)
- [x] 1.5 Register MarkdownService as singleton in Program.cs
## 2. Chat Component Integration
- [x] 2.1 Inject MarkdownService into Chat.razor
- [x] 2.2 For assistant messages, replace `@message.Content` with `@((MarkupString)MarkdownService.ConvertToHtml(message.Content))`
- [x] 2.3 Keep user messages rendering as plain text via `@message.Content`
- [x] 2.4 Verify streaming still works — markdown re-renders as tokens arrive
## 3. CSS Styling
- [x] 3.1 Add CSS for rendered markdown inside assistant bubbles: code blocks (background, padding, border-radius, overflow-x auto), inline code (background, padding), paragraphs (margin control), lists (indentation), tables (borders, header styling), blockquotes (left border, padding), links (color, underline)
- [x] 3.2 Ensure code blocks do not overflow bubble width — add horizontal scroll
- [x] 3.3 Ensure last paragraph/element in bubble has no bottom margin (tight fit)
## 4. Testing
- [x] 4.1 Add unit tests for MarkdownService: bold/italic, code blocks, lists, headings, tables, links
- [x] 4.2 Add sanitization tests: script tags stripped, event handlers stripped, safe tags preserved
- [x] 4.3 Manual test: send a message asking LLM to respond with markdown formatting and verify rendering

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-05

View File

@@ -0,0 +1,42 @@
## Context
The app currently uses a minimal MudLayout with just MudAppBar (Dense) + MudMainContent, and a single page at `/`. To support multiple pages, we need standard MudBlazor navigation: a collapsible MudDrawer with a NavMenu component.
## Goals / Non-Goals
**Goals:**
- Add collapsible MudDrawer with hamburger toggle in the AppBar
- Create a NavMenu component with a "Sales Assistant" link
- Move chat page to `/sales-assistant` route
- Maintain the Blazor tutorial style with inline comments
**Non-Goals:**
- Adding multiple pages beyond the existing chat (just the navigation structure)
- Changing the AppBar from Dense to regular
- Adding a default landing page (redirect `/``/sales-assistant` instead)
## Decisions
### MudDrawer configuration
- **Variant**: `DrawerVariant.Mini` — collapses to icon-width rather than fully hiding, so the user always sees the nav rail
- **Alternative considered**: `DrawerVariant.Responsive` — auto-hides on small screens. Rejected because Mini gives a more consistent desktop experience and the app is desktop-first.
- **ClipMode**: `DrawerClipMode.Always` — drawer sits below the AppBar, not beside it
### NavMenu as separate component
- Extract `NavMenu.razor` into `Layout/` alongside MainLayout rather than inlining nav links
- This is standard Blazor project structure and keeps MainLayout focused on shell layout
- The NavMenu will use `MudNavMenu` with `MudNavLink` items
### Route change: `/` → `/sales-assistant`
- The chat page moves to `/sales-assistant` to match navigation naming
- Add a redirect component at `/` that navigates to `/sales-assistant` on init
- This avoids a blank landing page while keeping the URL structure clean
### AppBar hamburger toggle
- Add `MudIconButton` with `Icons.Material.Filled.Menu` as the first element in the AppBar
- Toggle `_drawerOpen` bool that binds to `MudDrawer.Open`
## Risks / Trade-offs
- **Chat container height**: Currently uses `calc(100vh - 48px)` assuming Dense AppBar (48px). MudDrawer with ClipMode.Always doesn't affect vertical calc, so this should remain correct. Verify after implementation.
- **Breaking bookmarks**: Anyone bookmarking `/` will need to update to `/sales-assistant`. Mitigated by the redirect at `/`.

View File

@@ -0,0 +1,26 @@
## Why
The app currently has no navigation — just a single page at `/`. Adding a sidebar drawer with navigation enables the app to grow to multiple pages while providing standard MudBlazor layout structure (AppBar + Drawer + MainContent).
## What Changes
- Add a MudDrawer to MainLayout with a NavMenu component containing a "Sales Assistant" link
- Add a hamburger toggle button in the AppBar to open/close the drawer
- Move the existing chat page from `/` to `/sales-assistant`
- Add a landing page or redirect at `/` so the app has a default route
## Capabilities
### New Capabilities
- `sidebar-navigation`: Collapsible sidebar drawer with navigation menu, hamburger toggle, and route structure
### Modified Capabilities
- `chat-ui`: Route changes from `/` to `/sales-assistant`
## Impact
- **MainLayout.razor**: Add MudDrawer, hamburger icon, drawer toggle state
- **New NavMenu component**: Shared/NavMenu.razor with MudNavGroup/MudNavLink items
- **Chat.razor**: Route changes from `@page "/"` to `@page "/sales-assistant"`
- **Chat.razor.css**: Height calc may need adjustment if AppBar Dense changes
- No new packages — MudDrawer/MudNavMenu are part of MudBlazor

View File

@@ -0,0 +1,15 @@
## MODIFIED Requirements
### Requirement: Chat page is default route
The chat page SHALL be routed at `/sales-assistant`. The root URL (`/`) SHALL redirect to `/sales-assistant`.
#### Scenario: App opens to chat via redirect
- **WHEN** the user navigates to the root URL `/`
- **THEN** the browser redirects to `/sales-assistant` and the chat page is displayed
#### Scenario: Direct navigation to sales-assistant
- **WHEN** the user navigates to `/sales-assistant`
- **THEN** the chat page is displayed

View File

@@ -0,0 +1,38 @@
## ADDED Requirements
### Requirement: Collapsible sidebar drawer
The application SHALL have a MudDrawer in MainLayout that contains a navigation menu. The drawer SHALL be toggleable via a hamburger icon button in the AppBar.
#### Scenario: Drawer visible on load
- **WHEN** the application loads
- **THEN** the sidebar drawer is displayed in its default open state with navigation links visible
#### Scenario: Drawer toggles on hamburger click
- **WHEN** the user clicks the hamburger icon in the AppBar
- **THEN** the drawer toggles between open and collapsed states
### Requirement: Navigation menu with Sales Assistant link
The sidebar drawer SHALL contain a MudNavMenu with a "Sales Assistant" navigation link that routes to `/sales-assistant`.
#### Scenario: Sales Assistant link present
- **WHEN** the drawer is open
- **THEN** a "Sales Assistant" link with a SmartToy icon is visible in the navigation menu
#### Scenario: Clicking Sales Assistant navigates to chat
- **WHEN** the user clicks the "Sales Assistant" link
- **THEN** the browser navigates to `/sales-assistant` and the chat page renders in MudMainContent
### Requirement: NavMenu is a separate component
The navigation menu SHALL be implemented as a separate `NavMenu.razor` component in the Layout folder, referenced from MainLayout.
#### Scenario: NavMenu renders inside drawer
- **WHEN** MainLayout renders
- **THEN** the NavMenu component renders inside the MudDrawer with its navigation links

View File

@@ -0,0 +1,29 @@
## 1. MainLayout: Add Drawer and Hamburger Toggle
- [x] 1.1 Add a `_drawerOpen` bool field (default `true`) and a `ToggleDrawer` method to MainLayout.razor
- [x] 1.2 Add a `MudIconButton` with `Icons.Material.Filled.Menu` as the first element in the MudAppBar, wired to `ToggleDrawer`
- [x] 1.3 Add a `MudDrawer` with `Open="@_drawerOpen"`, `ClipMode="DrawerClipMode.Always"`, `Elevation="2"` inside the MudLayout, before MudMainContent
- [x] 1.4 Reference `<NavMenu />` inside the MudDrawer
## 2. NavMenu Component
- [x] 2.1 Create `Layout/NavMenu.razor` with a `MudNavMenu` containing a single `MudNavLink` — text "Sales Assistant", icon `Icons.Material.Filled.SmartToy`, href `/sales-assistant`
- [x] 2.2 Add inline tutorial comments explaining MudNavMenu, MudNavLink, and how href-based navigation works in Blazor
## 3. Chat Page Route Change
- [x] 3.1 Change `Chat.razor` route from `@page "/"` to `@page "/sales-assistant"`
- [x] 3.2 Update the page title from "Chat Agent" to "Sales Assistant"
## 4. Root Redirect
- [x] 4.1 Create a `Pages/Index.razor` component at `@page "/"` that redirects to `/sales-assistant` on initialization using `NavigationManager.NavigateTo` (Home.razor already exists as health check page at /health)
## 5. Chat Container Height Adjustment
- [x] 5.1 Verify `Chat.razor.css` height calc still works with the drawer layout — Dense AppBar is 48px, drawer does not affect vertical space. Adjust if needed.
## 6. Verification
- [x] 6.1 Build the client project (`dotnet build`) and confirm no compilation errors
- [x] 6.2 Run existing tests (`dotnet test`) and confirm they pass

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-06

View File

@@ -0,0 +1,63 @@
## Context
The extraction endpoint (`POST /api/chat/extract`) and few-shot prompting infrastructure exist on the backend. The client needs a way to trigger extraction by uploading email files and then handle the multi-turn extraction conversation (disambiguation, result presentation).
The current Chat.razor has a text input with send button and Enter key handling. The extraction flow adds a new input modality (file drop) and conversation mode tracking.
## Goals / Non-Goals
**Goals:**
- Enable email file upload via drag-and-drop and file picker
- Route uploaded emails to the extraction endpoint
- Support disambiguation follow-up within the same conversation
- Present extraction results clearly in the chat stream
**Non-Goals:**
- .msg file parsing (MVP accepts .html only — users save emails as HTML first)
- Structured result UI (tables, editable fields) — the agent streams formatted text/markdown
- Offline/batch extraction of multiple emails
- Email preview or parsing on the client before sending to the API
## Decisions
### 1. Drag-and-drop on the message area, not a separate upload zone
**Decision:** The entire chat message area (`.message-list`) acts as the drop target, with visual feedback when a file is dragged over.
**Why:** Drag-and-drop onto the conversation area is the natural gesture — it mirrors how you'd "give" a document to someone in a chat. A separate upload widget adds visual clutter.
**Alternative considered:** Dedicated upload zone above the input area. Rejected — takes permanent screen space for an occasional action.
### 2. HTML files only for MVP
**Decision:** Accept `.html` files only. Do not support `.msg` parsing in this change.
**Why:** .msg is a proprietary Microsoft format requiring either a server-side library (like MsgReader) or a JS parser. HTML is what Outlook "Save As" produces and is trivially read via the File API. Supporting .msg can be a follow-up change.
### 3. Conversation mode tracking with `_isExtractionMode`
**Decision:** Add a boolean `_isExtractionMode` flag to Chat.razor. When an email is uploaded, set it to `true`. All subsequent `SendMessage()` calls route to the extraction endpoint (passing the original email HTML + growing message list). "New Chat" resets to `false`.
**Why:** After initial extraction, the user needs to reply to disambiguation questions. Those replies must go to the extraction endpoint with full context, not the general chat endpoint. The mode flag is the simplest routing mechanism.
**Alternative considered:** Separate extraction page/component. Rejected — breaks the conversational flow and duplicates the chat UI.
### 4. File reading via Blazor JS interop
**Decision:** Use `InputFile` component or JavaScript interop with the File API to read the dropped file's text content. Send the HTML string to the extraction endpoint.
**Why:** Blazor WASM has `InputFile` for file picker but drag-and-drop requires JS interop for the `drop` event. We need both: `InputFile` for the button, JS interop for drag-and-drop.
### 5. Agent streams results as markdown, no special result UI
**Decision:** The extraction agent's response (including the final JSON table) streams as markdown rendered by the existing rich text display. No special result component.
**Why:** The rich text rendering already handles tables, code blocks, and formatted output. The agent naturally presents results as a markdown table. A structured result component adds complexity without clear UX benefit at this stage.
## Risks / Trade-offs
**[Large email files]** → HTML emails with embedded images can be large. Mitigation: the API receives the HTML string only — embedded images are base64 in the HTML and the LLM will ignore them. If size becomes an issue, strip image tags client-side before sending.
**[Mode confusion]** → User may not realize they're in extraction mode. Mitigation: show a visual indicator (e.g., chip or banner) when `_isExtractionMode` is true, and include "New Chat" to reset.
**[Drop event handling in Blazor]** → Blazor's built-in event handling for drag-and-drop is limited. Mitigation: use a small JS interop function for the drop handler that reads the file and calls back into .NET.

View File

@@ -0,0 +1,29 @@
## Why
The extraction agent and few-shot prompting infrastructure exist on the backend, but the chat UI has no way to send emails to the extraction endpoint. Users need to drag-and-drop or upload email files (.html) to trigger extraction. The client must route email uploads to `POST /api/chat/extract` and handle the conversational extraction flow, including disambiguation questions from the agent and result presentation.
## What Changes
- **Add drag-and-drop zone** to `Chat.razor` that accepts email files (.html)
- **Add file picker button** as an alternative upload method
- **Route uploaded emails** to the extraction endpoint via `ChatApiClient.SendExtractionStreamingAsync()`
- **Handle extraction conversation flow** — initial extraction streams in, user can reply to disambiguation questions, follow-ups continue via the extraction endpoint
- **Present extraction results** — the agent's streamed response includes formatted output; optionally add a "Copy JSON" action
- **Track conversation mode** — after an email upload, subsequent messages route to the extraction endpoint until "New Chat" resets to general mode
## Capabilities
### New Capabilities
- `email-upload`: Defines the drag-and-drop upload zone, file handling, visual feedback, and supported formats
- `extraction-conversation-flow`: Defines the client-side conversation mode tracking, routing between general chat and extraction, and result presentation
### Modified Capabilities
- `chat-ui`: Add the upload zone to the chat input area and track conversation mode (general vs extraction)
## Impact
- **UI changes**: New drop zone and upload button in Chat.razor, visual feedback during drag-over
- **Chat.razor.css**: Styling for drop zone states (idle, drag-over, uploading)
- **ChatApiClient**: Already has `SendExtractionStreamingAsync` from the previous change — this change wires it to the UI
- **Conversation state**: New `_isExtractionMode` flag in Chat.razor to route messages correctly
- **Depends on**: `update-extraction-schema` and `few-shot-prompt-infrastructure` (extraction endpoint must exist)

View File

@@ -0,0 +1,30 @@
## MODIFIED Requirements
### Requirement: Message input
The chat page SHALL provide a text input area at the bottom of the page where the user can type and submit messages. The input area SHALL also include a file upload button for triggering email extraction.
#### Scenario: Submit via button
- **WHEN** the user types text and clicks the send button
- **THEN** the message is added to the conversation and the input is cleared
#### Scenario: Submit via Enter key
- **WHEN** the user types text and presses Enter
- **THEN** the message is submitted (same as clicking send)
#### Scenario: Empty input blocked
- **WHEN** the user attempts to send an empty or whitespace-only message
- **THEN** nothing is sent and no message is added
#### Scenario: Input disabled during streaming
- **WHEN** the assistant is currently streaming a response
- **THEN** the input field, send button, and upload button are disabled until streaming completes
#### Scenario: Upload button opens file picker
- **WHEN** the user clicks the upload button in the input area
- **THEN** a file picker dialog opens filtered to .html files

View File

@@ -0,0 +1,38 @@
## ADDED Requirements
### Requirement: Drag-and-drop email upload
The chat message area SHALL accept files dragged from the desktop or file explorer. When a supported file is dropped, the client SHALL read the file content and send it to the extraction endpoint.
#### Scenario: Drag HTML file onto chat
- **WHEN** the user drags an .html file over the message area
- **THEN** a visual drop indicator appears (e.g., highlighted border, overlay text "Drop email here")
#### Scenario: Drop HTML file triggers extraction
- **WHEN** the user drops an .html file onto the message area
- **THEN** the client reads the HTML content, sends it to `POST /api/chat/extract`, and streams the extraction response in the chat
#### Scenario: Unsupported file type rejected
- **WHEN** the user drops a non-.html file (e.g., .pdf, .docx)
- **THEN** the client shows a brief error message indicating only .html files are supported
### Requirement: File picker upload button
The chat input area SHALL include an upload button (e.g., attachment icon) that opens a file picker dialog for selecting .html email files.
#### Scenario: Upload via file picker
- **WHEN** the user clicks the upload button and selects an .html file
- **THEN** the client reads the HTML content and sends it to the extraction endpoint, same as drag-and-drop
### Requirement: Upload disabled during streaming
The upload zone and file picker SHALL be disabled while a response is streaming.
#### Scenario: Drop during streaming
- **WHEN** the user attempts to drop a file while the assistant is streaming
- **THEN** the drop is ignored and no extraction request is sent

View File

@@ -0,0 +1,47 @@
## ADDED Requirements
### Requirement: Extraction mode tracking
The chat page SHALL track whether the current conversation is in extraction mode. Extraction mode is entered when an email is uploaded and exited when the user starts a new chat.
#### Scenario: Enter extraction mode on upload
- **WHEN** the user uploads an email file
- **THEN** the conversation enters extraction mode and subsequent messages are routed to the extraction endpoint
#### Scenario: Exit extraction mode on New Chat
- **WHEN** the user clicks "New Chat" while in extraction mode
- **THEN** the conversation exits extraction mode and returns to general chat routing
### Requirement: Extraction mode visual indicator
The chat page SHALL display a visual indicator when in extraction mode so the user knows their messages are part of an extraction conversation.
#### Scenario: Indicator shown in extraction mode
- **WHEN** the conversation is in extraction mode
- **THEN** a visual indicator (e.g., chip, banner, or subtitle) is visible showing the extraction context
#### Scenario: Indicator hidden in general mode
- **WHEN** the conversation is in general chat mode
- **THEN** no extraction indicator is shown
### Requirement: Follow-up messages route to extraction endpoint
In extraction mode, text messages typed by the user SHALL be sent to the extraction endpoint with the original email HTML and full conversation history, not to the general chat endpoint.
#### Scenario: User replies to disambiguation question
- **WHEN** the agent asks "Which legal entity?" and the user types "1"
- **THEN** the client sends an ExtractionRequest with the original email HTML plus all messages (assistant question + user reply) to `POST /api/chat/extract`
### Requirement: Email upload message in chat
When an email is uploaded, the chat SHALL display a user message indicating the upload (e.g., showing the filename) before the extraction response streams in.
#### Scenario: Upload message displayed
- **WHEN** the user drops "trade_request.html"
- **THEN** a user message appears in the chat like "[Uploaded: trade_request.html]" followed by the streaming extraction response

View File

@@ -0,0 +1,41 @@
## 1. Drag-and-Drop Infrastructure
- [x] 1.1 Add JS interop function for drag-and-drop file reading — a small JS function that listens for `dragover`/`drop` events on a given element, reads the dropped file as text, and invokes a .NET callback with the filename and content
- [x] 1.2 Add the JS interop script to `wwwroot/index.html` or a separate `.js` file referenced there
- [x] 1.3 Wire the drag-and-drop JS interop to the `.message-list` element in Chat.razor — register on `OnAfterRenderAsync`, dispose on component disposal
## 2. File Upload Button
- [x] 2.1 Add `MudIconButton` with attachment icon next to the send button in the input area
- [x] 2.2 Add hidden `InputFile` component accepting `.html` files, triggered by the icon button click
- [x] 2.3 Handle `InputFile.OnChange` — read the selected file content as string, trigger extraction
## 3. Drop Zone Visual Feedback
- [x] 3.1 Add `_isDragOver` boolean state to Chat.razor, toggled by dragenter/dragleave events from JS interop
- [x] 3.2 Add CSS class `.drag-over` to `.message-list` when `_isDragOver` is true — highlighted border, subtle overlay with "Drop email here" text
- [x] 3.3 Add `.drag-over` styles to Chat.razor.css
## 4. Extraction Mode and Routing
- [x] 4.1 Add `_isExtractionMode` boolean and `_emailHtml` string fields to Chat.razor
- [x] 4.2 When an email file is read (via drop or file picker): set `_isExtractionMode = true`, store email HTML in `_emailHtml`, add a user message showing "[Uploaded: filename.html]"
- [x] 4.3 Create `SendExtractionMessage()` method — builds `ExtractionRequest` with `_emailHtml` and conversation messages, calls `ChatApiClient.SendExtractionStreamingAsync()`, streams response into assistant message (same pattern as `SendMessage()`)
- [x] 4.4 Modify `SendMessage()` — if `_isExtractionMode`, build `ExtractionRequest` with `_emailHtml` + all messages and call the extraction endpoint instead of the chat endpoint
- [x] 4.5 On file drop/upload: call `SendExtractionMessage()` for the initial extraction
- [x] 4.6 Modify `NewChat()` to reset `_isExtractionMode = false` and `_emailHtml = ""`
## 5. Extraction Mode Indicator
- [x] 5.1 Add a `MudChip` or small banner below the tab header showing "Extraction Mode" when `_isExtractionMode` is true
- [x] 5.2 Style the indicator in Chat.razor.css
## 6. Guard Rails
- [x] 6.1 Reject non-.html files in both drop handler and InputFile handler — show a snackbar or inline message
- [x] 6.2 Disable drop zone and file picker during streaming (`_isStreaming` flag)
## 7. Build and Verify
- [x] 7.1 Build the solution (`dotnet build`) and confirm no compilation errors
- [x] 7.2 Run all tests (`dotnet test`) and confirm they pass

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-05

View File

@@ -0,0 +1,51 @@
## Context
The chat page currently has a single-panel layout: message list + input. The system prompt is absent (no system message in ChatHistory), and model parameters like temperature use Semantic Kernel defaults. For prompt engineering and debugging, these need to be editable in the UI without restarting the server.
## Goals / Non-Goals
**Goals:**
- Tabbed UI: Chat, System Prompt, Model Settings — all on the same page
- System prompt and model settings sent with each chat request
- Backend applies them to SK's ChatHistory and OpenAIPromptExecutionSettings
- Settings persist in the browser session (survive tab switches, not page reloads)
**Non-Goals:**
- Persisting settings to disk or server (future — save/load prompt profiles)
- Phase 2 prompt templates and few-shot examples (scoped in proposal, not implemented here)
- Changing the SSE streaming contract
## Decisions
### Tabbed layout using MudTabs
- Use `MudTabs` with `MudTabPanel` for each section: Chat, System Prompt, Model Settings
- **Alternative considered**: MudDrawer panels or separate pages. Rejected because tabs keep everything on one page — switching between prompt and chat should be instant with no navigation.
- The Chat tab contains the existing message list and input (unchanged)
- System Prompt tab: a `MudTextField` with `Lines="10"` for multi-line editing
- Model Settings tab: `MudNumericField` or `MudSlider` for Temperature (0.02.0), TopP (0.01.0), MaxTokens (14096)
### Settings sent per-request, not stored server-side
- `ChatRequest` gains optional `SystemPrompt` (string?) and `Settings` (ModelSettings?) properties
- Backend treats them as nullable — if absent, defaults apply (no system prompt, SK default temperature)
- This keeps the API stateless and avoids server-side session management
- **Alternative considered**: Server-side settings endpoint. Rejected — adds complexity for a single-user debugging tool.
### ModelSettings as a shared DTO
- New `ModelSettings.cs` in Shared/Models with `Temperature` (double?), `TopP` (double?), `MaxTokens` (int?)
- All fields nullable — only set values override defaults
- Maps directly to `OpenAIPromptExecutionSettings` properties on the backend
### System prompt applied as ChatHistory system message
- `chatHistory.AddSystemMessage(request.SystemPrompt)` as the first entry before user/assistant messages
- SK and OpenAI APIs treat the system message as behavioral instructions for the model
### Tab state persists in component fields
- `_systemPrompt` and `_modelSettings` are component-level fields, not per-tab
- Switching tabs doesn't reset values (MudTabs preserves panel content by default)
- Values are lost on page refresh — acceptable for a debugging tool
## Risks / Trade-offs
- **[Tab switch loses scroll position]** → MudTabs renders all panels but hides inactive ones, so scroll position in the chat tab is preserved
- **[Large system prompts inflate request size]** → Acceptable for single-user debugging; no size limit enforced
- **[Temperature/TopP interaction]** → Standard OpenAI behavior: setting both is allowed but not recommended. Show a note in the UI, don't enforce.

View File

@@ -0,0 +1,35 @@
## Why
When testing and debugging the AI chat agent, the system prompt and model parameters (temperature, top-p, max tokens) are hardcoded or absent. Exposing these in the UI lets the developer iterate on prompt engineering without restarting the server, and makes the app useful as a prompt testing workbench.
## What Changes
### Phase 1: Expose system prompt and model parameters
- Add tabbed UI to the chat page: **Chat** tab (existing conversation), **System Prompt** tab (editable text area), **Model Settings** tab (temperature, top-p, max tokens sliders/inputs)
- Extend the API contract: `ChatRequest` gains optional `SystemPrompt` and `ModelSettings` fields
- Backend applies the system prompt as the first message in ChatHistory and passes model settings to execution settings
### Phase 2: Prompt templates with few-shot examples (future)
- System prompt becomes a template with placeholder variables
- UI for adding few-shot input/output example pairs
- Template engine generates the final system prompt from template + examples
- *Phase 2 is scoped but NOT implemented in this change*
## Capabilities
### New Capabilities
- `prompt-settings-ui`: Tabbed interface for system prompt editing and model parameter controls
- `prompt-settings-api`: API contract extensions for system prompt and model parameters
### Modified Capabilities
- `chat-ui`: Chat page changes from single-panel to tabbed layout
- `chat-streaming`: API accepts optional system prompt and model settings in the request
## Impact
- **ChatRequest.cs** (Shared): Add `SystemPrompt` and `ModelSettings` properties
- **New ModelSettings.cs** (Shared): Temperature, TopP, MaxTokens model
- **ChatController.cs** (API): Apply system prompt to ChatHistory, pass model settings to execution settings
- **Chat.razor** (Client): Wrap in MudTabs, add System Prompt and Model Settings tab panels
- **ChatApiClient.cs** (Client): Pass new fields in requests
- No new packages — MudTabs, MudTextField, MudSlider are all part of MudBlazor

View File

@@ -0,0 +1,20 @@
## MODIFIED Requirements
### Requirement: Chat endpoint proxies to Responses API
The API backend SHALL expose `POST /api/chat` that accepts a `ChatRequest` containing messages, an optional system prompt, and optional model settings. The request is processed using a Semantic Kernel chat completion service. When a system prompt is provided, it SHALL be added as the first system message in the ChatHistory. When model settings are provided, non-null values SHALL be applied to the execution settings.
#### Scenario: Successful chat request with system prompt
- **WHEN** the client sends a POST to `/api/chat` with messages and a system prompt
- **THEN** the API creates a ChatHistory with the system prompt as the first message, followed by the conversation messages, and processes them through Semantic Kernel
#### Scenario: Successful chat request with model settings
- **WHEN** the client sends a POST to `/api/chat` with messages and model settings (e.g., Temperature=0.3)
- **THEN** the API applies the settings to OpenAIPromptExecutionSettings before calling the Semantic Kernel
#### Scenario: Successful chat request without optional fields
- **WHEN** the client sends a POST to `/api/chat` with only messages (no system prompt, no settings)
- **THEN** the API processes the request with default behavior (no system message, default execution settings)

View File

@@ -0,0 +1,15 @@
## MODIFIED Requirements
### Requirement: Chat page is default route
The chat page SHALL be routed at `/sales-assistant` (or `/` with redirect). The page content SHALL be wrapped in a MudTabs container with the conversation UI in the first tab panel.
#### Scenario: Page loads with chat tab active
- **WHEN** the user navigates to the chat page
- **THEN** the Chat tab is active showing the message list and input area
#### Scenario: Chat functionality unchanged
- **WHEN** the user sends a message from the Chat tab
- **THEN** the assistant response streams in exactly as before, with the same SSE contract and rendering behavior

View File

@@ -0,0 +1,43 @@
## ADDED Requirements
### Requirement: ModelSettings shared model
The Shared project SHALL define a `ModelSettings` class with nullable properties: `Temperature` (double?), `TopP` (double?), `MaxTokens` (int?). Null values indicate "use server default".
#### Scenario: All fields null
- **WHEN** a ModelSettings instance has all null fields
- **THEN** the backend uses Semantic Kernel default values for all parameters
#### Scenario: Partial override
- **WHEN** a ModelSettings instance has Temperature set but TopP and MaxTokens null
- **THEN** only Temperature is overridden; other parameters use defaults
### Requirement: System prompt in chat request
The `ChatRequest` SHALL accept an optional `SystemPrompt` (string?) property. When present and non-empty, the backend SHALL insert it as the first system message in the ChatHistory before user/assistant messages.
#### Scenario: System prompt provided
- **WHEN** a ChatRequest includes a non-empty SystemPrompt
- **THEN** the ChatHistory starts with a system message containing that text, followed by the conversation messages
#### Scenario: System prompt absent
- **WHEN** a ChatRequest has a null or empty SystemPrompt
- **THEN** the ChatHistory contains only user and assistant messages (no system message)
### Requirement: Model settings in chat request
The `ChatRequest` SHALL accept an optional `Settings` (ModelSettings?) property. When present, the backend SHALL apply non-null values to `OpenAIPromptExecutionSettings` before calling the Semantic Kernel.
#### Scenario: Temperature override
- **WHEN** a ChatRequest includes Settings with Temperature = 0.5
- **THEN** the OpenAIPromptExecutionSettings.Temperature is set to 0.5
#### Scenario: No settings provided
- **WHEN** a ChatRequest has null Settings
- **THEN** the backend uses default OpenAIPromptExecutionSettings (only FunctionChoiceBehavior.Auto is set)

View File

@@ -0,0 +1,53 @@
## ADDED Requirements
### Requirement: System prompt editor tab
The chat page SHALL include a "System Prompt" tab with a multi-line text area where the user can enter a system prompt. The system prompt value SHALL persist across tab switches within the same session.
#### Scenario: User enters a system prompt
- **WHEN** the user navigates to the System Prompt tab and types text
- **THEN** the text is stored in the component state and included in the next chat request
#### Scenario: System prompt survives tab switch
- **WHEN** the user enters a system prompt, switches to the Chat tab, then switches back
- **THEN** the system prompt text is unchanged
### Requirement: Model settings tab
The chat page SHALL include a "Model Settings" tab with controls for Temperature, TopP, and MaxTokens. Each control SHALL display its current value and allow adjustment within valid ranges.
#### Scenario: Temperature control
- **WHEN** the user adjusts the Temperature control
- **THEN** the value is constrained to 0.02.0 and included in the next chat request's settings
#### Scenario: TopP control
- **WHEN** the user adjusts the TopP control
- **THEN** the value is constrained to 0.01.0 and included in the next chat request's settings
#### Scenario: MaxTokens control
- **WHEN** the user sets the MaxTokens value
- **THEN** the value is constrained to 14096 and included in the next chat request's settings
#### Scenario: Default values
- **WHEN** the user has not changed any model settings
- **THEN** the controls show default values (Temperature: 1.0, TopP: 1.0, MaxTokens: empty/unset) and no overrides are sent to the API
### Requirement: Tabbed page layout
The chat page SHALL use MudTabs with three tab panels: "Chat" (the existing conversation UI), "System Prompt" (the prompt editor), and "Model Settings" (the parameter controls).
#### Scenario: Chat tab is default
- **WHEN** the page loads
- **THEN** the Chat tab is active and the conversation UI is displayed
#### Scenario: Tab switching
- **WHEN** the user clicks a different tab
- **THEN** the corresponding panel is displayed and the previous panel is hidden but retains its state

View File

@@ -0,0 +1,40 @@
## 1. Shared Models
- [x] 1.1 Create `ModelSettings.cs` in Shared/Models — `double? Temperature`, `double? TopP`, `int? MaxTokens`
- [x] 1.2 Add `string? SystemPrompt` and `ModelSettings? Settings` properties to `ChatRequest.cs`
## 2. API Backend
- [x] 2.1 Update `ChatController.Post()` — if `request.SystemPrompt` is non-empty, call `chatHistory.AddSystemMessage(request.SystemPrompt)` before adding user/assistant messages
- [x] 2.2 Update `ChatController.Post()` — if `request.Settings` is non-null, apply non-null Temperature, TopP, MaxTokens to `OpenAIPromptExecutionSettings`
## 3. Client UI — Tabbed Layout
- [x] 3.1 Wrap the existing Chat.razor content (chat-container div) inside `<MudTabs>` with three `<MudTabPanel>` elements: "Chat", "System Prompt", "Model Settings"
- [x] 3.2 Add component fields: `_systemPrompt` (string), `_temperature` (double?), `_topP` (double?), `_maxTokens` (int?)
## 4. System Prompt Tab
- [x] 4.1 Add a `MudTextField` with `Lines="10"`, `Variant="Variant.Outlined"`, bound to `_systemPrompt`, with placeholder text explaining what a system prompt does
- [x] 4.2 Include the `_systemPrompt` value in the `ChatRequest` built by `SendMessage()`
## 5. Model Settings Tab
- [x] 5.1 Add `MudNumericField<double?>` for Temperature (min 0.0, max 2.0, step 0.1) with label and helper text
- [x] 5.2 Add `MudNumericField<double?>` for TopP (min 0.0, max 1.0, step 0.1) with label and helper text
- [x] 5.3 Add `MudNumericField<int?>` for MaxTokens (min 1, max 4096) with label and helper text
- [x] 5.4 Include the model settings in the `ChatRequest` built by `SendMessage()`
## 6. Client Service
- [x] 6.1 Verify `ChatApiClient.SendChatStreamingAsync()` serializes the new `ChatRequest` fields correctly (SystemPrompt, Settings) — no changes expected since it already serializes the full object
## 7. Styling
- [x] 7.1 Adjust `Chat.razor.css` — the chat-container height calc needs to account for the MudTabs header height (~48px). The tabs header sits inside the content area below the AppBar.
## 8. Verification
- [x] 8.1 Build the solution (`dotnet build`) and confirm no compilation errors
- [x] 8.2 Run existing tests (`dotnet test`) and confirm they pass
- [x] 8.3 Update any tests that construct `ChatRequest` if the new nullable fields cause issues — no updates needed, nullable fields don't break existing tests

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-06

View File

@@ -0,0 +1,69 @@
## Context
The extraction agent (Semantic Kernel with auto-invoke tools) needs few-shot examples to reliably map sales emails to TradeItem JSON. The `update-extraction-schema` change provides the real schema and tools. This change adds the prompting infrastructure and a dedicated endpoint.
Up to 100 input/output example pairs are available. Research shows 3-5 well-chosen examples are optimal for few-shot prompting — more can degrade performance by consuming context and introducing noise.
## Goals / Non-Goals
**Goals:**
- Load curated few-shot examples from disk and assemble a reusable ChatHistory prefix
- Provide a fixed instruction template for extraction (not user-editable)
- Create a dedicated extraction endpoint with the correct prompt and tools
- Keep the general chat endpoint unchanged
**Non-Goals:**
- Dynamic example selection (RAG-like similarity matching) — future enhancement
- Email upload UI (separate change: `email-upload-ux`)
- Building or curating the actual example content (user provides these)
- Evaluation pipeline for the ~95 non-few-shot examples (future work)
## Decisions
### 1. Examples as conversation turns (not system prompt string)
**Decision:** Inject few-shot examples as alternating User/Assistant messages in the ChatHistory, after the system message and before the real email.
**Why:** Chat models treat conversation turns as prior context — the model "sees" the examples as things it already did correctly. This is more effective than embedding examples in the system prompt string, where they're treated as instructions rather than demonstrated behavior.
### 2. Examples loaded once at startup, cached as ChatHistory prefix
**Decision:** `FewShotService` reads example files at startup, builds a `ChatHistory` prefix (system message + example turns), and caches it as a singleton. Each extraction request clones this prefix and appends the real email.
**Why:** Example files don't change at runtime. Loading once avoids repeated disk I/O. Cloning the cached prefix is cheap (ChatHistory is a list of message objects).
**Alternative considered:** Load examples per-request. Rejected — unnecessary I/O for static data.
### 3. Instruction template as an embedded text file
**Decision:** Store the extraction instruction template as a text file at `examples/extraction/instruction-template.txt`, loaded by FewShotService alongside the examples.
**Why:** Keeps the prompt text editable without recompilation. Co-located with the examples it references. Not in appsettings.json because it's multi-line prose, not configuration.
### 4. Separate extraction endpoint, not a mode flag on /api/chat
**Decision:** `POST /api/chat/extract` as a new controller action, separate from `POST /api/chat`.
**Why:** The extraction path uses a completely different ChatHistory (few-shot prefix, not user system prompt), different tools (extraction plugins only), and a different request DTO. A mode flag on the existing endpoint would add branching complexity. Separate endpoints make each path clear.
**Alternative considered:** Mode flag on ChatRequest (e.g. `"mode": "extract"`). Rejected — the request shapes diverge enough to warrant separate DTOs and endpoints.
### 5. ExtractionRequest includes conversation messages for follow-up
**Decision:** `ExtractionRequest` contains `EmailHtml` (string, the email to extract) plus `Messages` (list, optional follow-up conversation for disambiguation).
**Why:** After initial extraction, the agent may ask disambiguation questions. Follow-up user replies need to be sent back with the full conversation context so the agent can continue. The first request has only `EmailHtml`; subsequent requests include the growing `Messages` list.
### 6. Example folder uses numbered subdirectories
**Decision:** `examples/extraction/few-shot/01/input.html + output.json`, `02/`, etc.
**Why:** Numbered prefixes control ordering in the ChatHistory. Each subdirectory is one example, keeping input and output together. Easy to add/remove/reorder examples by renaming directories.
## Risks / Trade-offs
**[Example quality determines extraction quality]** → Poorly chosen few-shot examples will mislead the model. Mitigation: document selection criteria (diversity of swap structures, currencies, breakclause values). The user curates the 3-5 examples.
**[Instruction template drift]** → If the schema changes, the instruction template must be updated manually. Mitigation: the template references the TradeItem field names explicitly, making it obvious when they're out of sync.
**[ChatHistory size with few-shot examples]** → Each example adds ~2-5KB of tokens. With 5 examples that's ~10-25KB, well within model context limits. Not a risk at current scale but would be if dynamic selection adds more examples later.

View File

@@ -0,0 +1,31 @@
## Why
The extraction agent needs few-shot examples to reliably produce correct structured output from sales emails. Without examples, the agent relies entirely on the instruction template and tool descriptions, which cannot fully convey the implicit mapping conventions (date parsing from "OB" prefix, flattening swap legs, currency symbol mapping, breakclause defaults). A curated set of 3-5 input/output examples injected as conversation turns in the ChatHistory dramatically improves extraction accuracy. The remaining ~95 available examples serve as an evaluation set for offline quality testing.
Additionally, the extraction workflow needs a dedicated API endpoint separate from general chat, since it uses a different system prompt, different tools, and the few-shot ChatHistory prefix.
## What Changes
- **Create examples folder structure** at `examples/extraction/few-shot/` with numbered subdirectories, each containing `input.html` (email) and `output.json` (expected ExtractionResult)
- **Create extraction instruction template** — a fixed system prompt defining the extraction task, schema, and mapping rules (separate from the user-editable system prompt)
- **Create a FewShotService** that loads examples from disk at startup and pre-assembles a ChatHistory prefix (system message + alternating user/assistant turns)
- **Add `POST /api/chat/extract` endpoint** that uses the few-shot ChatHistory, appends the real email, and streams the extraction response via SSE
- **Create `ExtractionRequest` DTO** for the extraction endpoint (email content + optional follow-up messages for disambiguation)
- **Update client `ChatApiClient`** with a method for the extraction endpoint
## Capabilities
### New Capabilities
- `few-shot-prompting`: Defines the example folder structure, loading mechanism, ChatHistory assembly, and instruction template for few-shot extraction prompting
- `extraction-endpoint`: Defines the dedicated extraction API endpoint, its request/response contract, and how it differs from the general chat endpoint
### Modified Capabilities
- `chat-streaming`: Add the extraction endpoint alongside the existing chat endpoint, sharing the same SSE streaming contract
## Impact
- **New files**: examples folder, FewShotService, instruction template, ExtractionRequest DTO, extraction controller action
- **Configuration**: example folder path in appsettings.json
- **API surface**: new `POST /api/chat/extract` endpoint
- **Client**: new method on ChatApiClient (no UI changes — that's the email-upload-ux change)
- **Depends on**: `update-extraction-schema` (needs TradeItem schema for examples and validation tools)

View File

@@ -0,0 +1,25 @@
## MODIFIED Requirements
### Requirement: Chat endpoint proxies to Responses API
The API backend SHALL expose `POST /api/chat` that accepts a `ChatRequest` containing messages, an optional system prompt, and optional model settings. The request is processed using a Semantic Kernel chat completion service. When a system prompt is provided, it SHALL be added as the first system message in the ChatHistory. When model settings are provided, non-null values SHALL be applied to the execution settings. A separate `POST /api/chat/extract` endpoint SHALL handle extraction-specific requests with few-shot prompting.
#### Scenario: Successful chat request with system prompt
- **WHEN** the client sends a POST to `/api/chat` with messages and a system prompt
- **THEN** the API creates a ChatHistory with the system prompt as the first message, followed by the conversation messages, and processes them through Semantic Kernel
#### Scenario: Successful chat request with model settings
- **WHEN** the client sends a POST to `/api/chat` with messages and model settings (e.g., Temperature=0.3)
- **THEN** the API applies the settings to OpenAIPromptExecutionSettings before calling the Semantic Kernel
#### Scenario: Successful chat request without optional fields
- **WHEN** the client sends a POST to `/api/chat` with only messages (no system prompt, no settings)
- **THEN** the API processes the request with default behavior (no system message, default execution settings)
#### Scenario: Extraction request routed to dedicated endpoint
- **WHEN** the client sends a POST to `/api/chat/extract` with email HTML
- **THEN** the API uses the few-shot ChatHistory prefix and extraction tools instead of the general chat configuration

View File

@@ -0,0 +1,43 @@
## ADDED Requirements
### Requirement: Extraction API endpoint
The API SHALL expose `POST /api/chat/extract` that accepts an `ExtractionRequest` containing the email HTML content and optional follow-up conversation messages. The endpoint SHALL use the few-shot ChatHistory prefix (not the user-editable system prompt) and load extraction-specific SK plugins.
#### Scenario: Initial extraction request
- **WHEN** the client sends a POST to `/api/chat/extract` with email HTML and no follow-up messages
- **THEN** the API assembles the few-shot ChatHistory, appends the email as the final user message, and streams the extraction response via SSE
#### Scenario: Follow-up disambiguation request
- **WHEN** the client sends a POST to `/api/chat/extract` with email HTML and follow-up messages (e.g., user selecting a counterparty)
- **THEN** the API assembles the few-shot ChatHistory, appends the email, appends all follow-up messages, and streams the continuation response via SSE
#### Scenario: SSE streaming contract
- **WHEN** the extraction endpoint streams a response
- **THEN** it uses the same SSE format as `/api/chat`: `data: {"text":"..."}\n\n` for deltas and `data: [DONE]\n\n` for completion
### Requirement: ExtractionRequest DTO
The system SHALL define an `ExtractionRequest` class with `EmailHtml` (string, required) and `Messages` (List<ChatMessage>, optional) for follow-up conversation context.
#### Scenario: First request has email only
- **WHEN** the user uploads an email for the first time
- **THEN** the ExtractionRequest contains `EmailHtml` with the email content and an empty `Messages` list
#### Scenario: Follow-up request includes conversation
- **WHEN** the user replies to a disambiguation question
- **THEN** the ExtractionRequest contains the original `EmailHtml` plus `Messages` with the full assistant/user exchange since the extraction started
### Requirement: Extraction endpoint uses extraction tools only
The extraction endpoint SHALL import only the extraction-specific SK plugins (counterparty lookup, trade validation, currency validation, schema validation). General chat tools (if any) SHALL NOT be loaded for extraction requests.
#### Scenario: Tool isolation
- **WHEN** the extraction endpoint processes a request
- **THEN** only extraction-related KernelFunctions are available to the LLM

View File

@@ -0,0 +1,52 @@
## ADDED Requirements
### Requirement: Few-shot example folder structure
The system SHALL store few-shot examples at `examples/extraction/few-shot/` with numbered subdirectories (e.g., `01/`, `02/`). Each subdirectory SHALL contain `input.html` (the example email) and `output.json` (the expected ExtractionResult JSON).
#### Scenario: Example folder layout
- **WHEN** the application starts
- **THEN** it reads example pairs from `examples/extraction/few-shot/` in numeric directory order
#### Scenario: Adding a new example
- **WHEN** a new subdirectory (e.g., `04/`) is added with `input.html` and `output.json`
- **THEN** the new example is included in the few-shot ChatHistory prefix after the next application restart
### Requirement: Extraction instruction template
The system SHALL load a fixed instruction template from `examples/extraction/instruction-template.txt` that defines the extraction task, the TradeItem schema, and the mapping rules (date parsing, leg flattening, currency mapping, breakclause defaults). This template is NOT the user-editable system prompt.
#### Scenario: Template loaded at startup
- **WHEN** the application starts
- **THEN** the instruction template is loaded from disk and used as the system message in the extraction ChatHistory
#### Scenario: Template content
- **WHEN** the instruction template is loaded
- **THEN** it contains the TradeItem field definitions, expected JSON output format, and explicit mapping rules
### Requirement: ChatHistory assembly with few-shot examples
The system SHALL provide a `FewShotService` that assembles a reusable ChatHistory prefix at startup: the instruction template as a system message, followed by alternating User (input.html) and Assistant (output.json) messages for each example. Each extraction request SHALL clone this prefix and append the real email as the final user message.
#### Scenario: ChatHistory prefix structure
- **WHEN** the service assembles the prefix with 3 examples
- **THEN** the ChatHistory contains: 1 system message + 3 user messages + 3 assistant messages (7 messages total)
#### Scenario: Prefix cached and cloned per request
- **WHEN** an extraction request arrives
- **THEN** the service clones the cached prefix (not re-reading from disk) and appends the email content as a new user message
### Requirement: Evaluation example folder
The system SHALL support an `examples/extraction/evaluation/` folder for bulk examples used in offline testing. This folder is NOT loaded at startup and NOT used in the few-shot prompt.
#### Scenario: Evaluation folder ignored at runtime
- **WHEN** the application starts
- **THEN** it does not load examples from `examples/extraction/evaluation/`

View File

@@ -0,0 +1,36 @@
## 1. Example Folder Structure
- [x] 1.1 Create directory structure: `examples/extraction/few-shot/` and `examples/extraction/evaluation/`
- [x] 1.2 Add placeholder example `01/` with `input.html` (sample email HTML) and `output.json` (sample ExtractionResult JSON matching TradeItem schema). Use realistic but anonymized data.
- [x] 1.3 Add placeholder example `02/` with a different email pattern (e.g., single swap, different currency)
- [x] 1.4 Add placeholder example `03/` with an edge case (e.g., breakclause = "Y", or unusual counterparty name)
## 2. Instruction Template
- [x] 2.1 Create `examples/extraction/instruction-template.txt` with the fixed extraction system prompt: task description, TradeItem schema definition (all 7 fields with types), mapping rules (date parsing, leg flattening, currency symbol → ISO code, breakclause default), and expected JSON output format
## 3. FewShotService
- [x] 3.1 Create `FewShotService.cs` in the API project — constructor loads instruction template and all few-shot examples from disk, assembles a ChatHistory prefix (system message + alternating user/assistant turns)
- [x] 3.2 Add `CloneWithEmail(string emailHtml)` method that clones the cached ChatHistory prefix and appends the email as a user message
- [x] 3.3 Add `CloneWithEmailAndMessages(string emailHtml, List<ChatMessage> messages)` method for follow-up disambiguation requests — clones prefix, appends email, appends follow-up messages
- [x] 3.4 Register `FewShotService` as singleton in `Program.cs`
- [x] 3.5 Add `Examples:FewShotPath` configuration in `appsettings.json` pointing to the examples folder
## 4. ExtractionRequest DTO
- [x] 4.1 Create `ExtractionRequest.cs` in Shared/Models with `EmailHtml` (string, required) and `Messages` (List<ChatMessage>, optional)
## 5. Extraction Endpoint
- [x] 5.1 Add `Extract` action to `ChatController` (or new `ExtractionController`) — `POST /api/chat/extract` accepting `ExtractionRequest`
- [x] 5.2 In the Extract action: get ChatHistory from `FewShotService.CloneWithEmail()` or `CloneWithEmailAndMessages()` based on whether Messages are present
- [x] 5.3 Import extraction-specific plugins only (not general chat plugins)
- [x] 5.4 Stream response via SSE using the same format as the existing chat endpoint
- [x] 5.5 Update `ChatApiClient` on the client side — add `SendExtractionStreamingAsync(ExtractionRequest request)` method mirroring the existing streaming pattern
## 6. Build and Verify
- [x] 6.1 Build the solution (`dotnet build`) and confirm no compilation errors
- [x] 6.2 Run all tests (`dotnet test`) and confirm they pass
- [x] 6.3 Add unit test for `FewShotService` — verify it loads examples and assembles correct ChatHistory structure (message count, roles, ordering)

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-06

View File

@@ -0,0 +1,73 @@
## Context
The extraction pipeline currently uses a placeholder schema (`ExtractedFields` with Client, Project, Hours, Rate, Currency, Date) and a single in-process validation method. The real domain is CVA trade extraction from sales emails: HTML emails containing swap/leg tables need to be parsed into an array of `TradeItem` objects. Validation and enrichment require calling 3-5 existing external APIs (counterparty lookup, trade validation, etc.), some of which return multiple candidates requiring user disambiguation.
The `ExtractionPlugin` is already registered with Semantic Kernel and auto-invoked via `FunctionChoiceBehavior.Auto()`. The tool calling loop and SSE streaming infrastructure are in place.
## Goals / Non-Goals
**Goals:**
- Replace placeholder models with the real TradeItem schema
- Replace single validation method with multiple external API tool wrappers
- Support disambiguation workflow where tool results return candidate lists
- Keep the external API integration configurable and testable
**Non-Goals:**
- Few-shot prompting infrastructure (separate change: `few-shot-prompt-infrastructure`)
- Email upload/drag-drop UX (separate change: `email-upload-ux`)
- Building the external APIs themselves (they already exist)
- Changing the SSE streaming contract or client-side rendering
## Decisions
### 1. Schema structure: wrapper with items array
**Decision:** Use `ExtractionResult { List<TradeItem> Items }` rather than returning a flat `TradeItem`.
**Why:** A single email typically contains multiple swaps, each with multiple legs. Each leg becomes a separate `TradeItem`. The wrapper allows returning all items from one extraction pass.
**Alternative considered:** Returning `List<TradeItem>` directly. Rejected because a wrapper object is more extensible (could add metadata like extraction confidence, email subject, etc. later) and produces cleaner JSON (`{"items": [...]}` vs bare array).
### 2. TradeItem uses snake_case JSON property names
**Decision:** Use `[JsonPropertyName("valuedate")]` etc. to produce snake_case JSON output.
**Why:** The existing external APIs and downstream consumers expect snake_case. The C# properties will use PascalCase per convention, with JSON attributes for serialization.
### 3. One plugin class with multiple methods vs multiple plugin classes
**Decision:** Single `ExtractionPlugin` class with 3-5 `[KernelFunction]` methods.
**Why:** All tools serve the same extraction workflow. SK discovers functions by class, so a single class keeps registration simple (`ImportPluginFromObject`). If tools grow beyond 6-7, split into focused plugin classes.
**Alternative considered:** Separate plugin class per external API. Rejected as premature — adds registration complexity for no benefit at 3-5 methods.
### 4. External API calls via typed HttpClients
**Decision:** Register typed `HttpClient` instances for each external API via `AddHttpClient<T>()` in `Program.cs`. Each plugin method receives its client via constructor injection.
**Why:** Typed HttpClients are the standard ASP.NET Core pattern. They support per-client base URL configuration, DI, and are easily mockable in tests.
### 5. Disambiguation via tool return values, not special UI
**Decision:** When a tool like `lookup_counterparty` finds multiple matches, it returns the candidate list as JSON. The LLM agent sees the candidates and asks the user to choose via natural language in the streamed response.
**Why:** This uses the existing streaming conversation infrastructure with zero client changes. The agent is already conversational — it simply says "I found 3 matches, which one?" and the user replies.
**Alternative considered:** Structured disambiguation UI (radio buttons, dropdowns). Rejected for this change — adds client complexity and a new response type. Can be added later as a UX enhancement.
### 6. ExtractionPlugin conditionally loaded per request
**Decision:** Continue importing extraction plugins per-request in the controller (`_kernel.ImportPluginFromObject`), not at startup.
**Why:** General chat requests don't need extraction tools. Per-request import keeps the tool list clean for non-extraction conversations, reducing token usage and avoiding confusing the LLM with irrelevant tools.
## Risks / Trade-offs
**[External API availability]** → Plugin methods should handle HTTP errors gracefully and return structured error responses that the LLM can reason about (e.g., "Counterparty API unavailable, proceeding without legal entity lookup"). The agent can then inform the user.
**[Breaking change to ExtractedFields]** → Any code referencing the old schema breaks. Mitigation: this is a small codebase with known consumers. Update all references in the same change. Tests will catch missed spots.
**[LLM disambiguation quality]** → The agent must correctly interpret candidate lists and present clear choices. Mitigation: tool descriptions must be explicit about what the return values mean. Few-shot examples (next change) will reinforce the pattern.
**[External API response format coupling]** → Plugin methods parse external API responses. If those APIs change, plugins break. Mitigation: typed response DTOs with defensive deserialization. External APIs are stable and owned by the same team.

View File

@@ -0,0 +1,28 @@
## Why
The current extraction schema (`ExtractedFields`) uses placeholder fields (Client, Project, Hours, Rate, Currency, Date) that don't match the real domain. The actual use case is CVA (Credit Valuation Adjustment) trade extraction from sales emails — parsing HTML emails into structured trade items with fields like counterparty, trade_id, pv, and legal_entity. The single `ExtractionPlugin.ValidateExtractedFields()` method also needs to be replaced with multiple tools that wrap existing external APIs for counterparty lookup, trade validation, and other checks.
## What Changes
- **Replace `ExtractedFields.cs`** with real domain models: `ExtractionResult` (wrapper) and `TradeItem` (per-trade fields: valuedate, counterparty, legal_entity, trade_id, display_ccy, pv, breakclause)
- **Replace `ExtractionPlugin.cs`** single validation method with 3-5 SK plugin methods, each wrapping an existing external API (counterparty lookup, trade validation, currency validation, schema validation)
- **Update `ValidationResult.cs`** to support richer results — candidate lists for disambiguation, not just pass/fail
- **Add typed HttpClients** for the external validation/lookup APIs, configured via `appsettings.json`
- **Update existing tests** that reference the old `ExtractedFields` and `ExtractionPlugin`
## Capabilities
### New Capabilities
- `extraction-schema`: Defines the real TradeItem schema, ExtractionResult wrapper, and the mapping rules from email content to structured output (date format, flattening swap legs, breakclause defaults)
- `extraction-tools`: Defines the external API tool plugins — counterparty lookup (with disambiguation), trade validation, currency validation, and final schema validation
### Modified Capabilities
- `agent-extraction`: Update requirements to reference the real schema (TradeItem) instead of generic "predefined fields", and add disambiguation workflow where tool results require user selection (e.g., counterparty/legal_entity tuples)
## Impact
- **Shared models**: `ExtractedFields.cs` replaced — **BREAKING** for any code referencing old fields
- **API plugins**: `ExtractionPlugin.cs` rewritten with new method signatures — **BREAKING** for existing tool calling behavior
- **External dependencies**: New HTTP calls to existing external APIs (counterparty, trade, currency)
- **Configuration**: New `appsettings.json` entries for external API base URLs
- **Tests**: Existing extraction-related tests need rewriting against new schema and tools

Some files were not shown because too many files have changed in this diff Show More