cc-dev-template 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/install.js +82 -6
- package/package.json +1 -1
- package/src/agents/claude-md-agent.md +1 -1
- package/src/agents/execution-agent.md +2 -2
- package/src/agents/tdd-agent.md +1 -1
- package/src/mcp-servers/qa-server/README.md +120 -0
- package/src/mcp-servers/qa-server/package-lock.json +2015 -0
- package/src/mcp-servers/qa-server/package.json +28 -0
- package/src/mcp-servers/qa-server/src/index.ts +227 -0
- package/src/mcp-servers/qa-server/tsconfig.json +17 -0
- package/src/scripts/statusline.js +15 -85
- package/src/skills/create-agent-skills/references/principles.md +21 -0
- package/src/skills/orchestration/scripts/plan-status.js +5 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qa-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server that spawns a QA sub-agent for frontend visual inspection",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"qa-server": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsx src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@anthropic-ai/claude-agent-sdk": "^0.1.30",
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
18
|
+
"zod": "^3.24.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^20.0.0",
|
|
22
|
+
"tsx": "^4.7.0",
|
|
23
|
+
"typescript": "^5.3.0"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* QA Server - MCP server that spawns a QA sub-agent for frontend visual inspection
|
|
4
|
+
*
|
|
5
|
+
* This server exposes a single `inspect_frontend` tool that:
|
|
6
|
+
* 1. Takes a URL and task description
|
|
7
|
+
* 2. Spawns a Claude sub-agent with chrome-devtools MCP access
|
|
8
|
+
* 3. The sub-agent navigates, screenshots, clicks, and inspects
|
|
9
|
+
* 4. Returns a structured QA report
|
|
10
|
+
*
|
|
11
|
+
* Token savings: ~20k (26 browser tools) → ~500 (1 tool)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import {
|
|
17
|
+
CallToolRequestSchema,
|
|
18
|
+
ListToolsRequestSchema,
|
|
19
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
20
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
21
|
+
|
|
22
|
+
// QA Agent system prompt - follows ADR-002: explain WHY, positive framing, clear context
|
|
23
|
+
const QA_SYSTEM_PROMPT = `You are a QA engineer helping developers verify their frontend works correctly.
|
|
24
|
+
|
|
25
|
+
**Why this matters:** Catching visual bugs, console errors, and broken interactions early saves significant debugging time. Your detailed assessment helps developers ship with confidence.
|
|
26
|
+
|
|
27
|
+
**Your capabilities:** You can navigate pages, take screenshots to see the visual state, click elements, fill forms, and read console messages. Screenshots show you exactly what users see.
|
|
28
|
+
|
|
29
|
+
**What makes a great QA report:**
|
|
30
|
+
- Visual Assessment: Describe the layout, colors, and overall appearance
|
|
31
|
+
- Console Errors: List any errors found (or confirm none exist)
|
|
32
|
+
- Task Result: What happened when you performed the requested task
|
|
33
|
+
- Issues Found: Any problems discovered during inspection
|
|
34
|
+
- Overall: PASS or FAIL with a clear reason
|
|
35
|
+
|
|
36
|
+
Focus on what the developer needs to know: visual issues, broken functionality, and console errors. Be thorough in your inspection and clear in your reporting.`;
|
|
37
|
+
|
|
38
|
+
// Create MCP server
|
|
39
|
+
const server = new Server(
|
|
40
|
+
{
|
|
41
|
+
name: "qa-server",
|
|
42
|
+
version: "1.0.0",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
capabilities: {
|
|
46
|
+
tools: {},
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// List available tools
|
|
52
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
53
|
+
return {
|
|
54
|
+
tools: [
|
|
55
|
+
{
|
|
56
|
+
name: "inspect_frontend",
|
|
57
|
+
description:
|
|
58
|
+
"Spawn a QA sub-agent to visually inspect a frontend. The sub-agent can navigate, take screenshots, click elements, fill forms, and check console errors. Returns a structured QA report. Uses Opus for best visual understanding.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
url: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "URL to inspect (e.g., http://localhost:3000, https://example.com)",
|
|
65
|
+
},
|
|
66
|
+
task: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description:
|
|
69
|
+
"What to inspect or test (e.g., 'Verify the login form works', 'Check for visual regressions on the homepage', 'Test the checkout flow')",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
required: ["url", "task"],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Handle tool calls
|
|
80
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
81
|
+
if (request.params.name !== "inspect_frontend") {
|
|
82
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { url, task } = request.params.arguments as { url: string; task: string };
|
|
86
|
+
|
|
87
|
+
if (!url || !task) {
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: "Error: Both 'url' and 'task' parameters are required",
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
isError: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.error(`[QA Server] Starting inspection of ${url}`);
|
|
100
|
+
console.error(`[QA Server] Task: ${task}`);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// Build the task prompt - follows ADR-002: purpose-driven, positive framing
|
|
104
|
+
const taskPrompt = `
|
|
105
|
+
**QA Inspection Request**
|
|
106
|
+
|
|
107
|
+
URL: ${url}
|
|
108
|
+
Task: ${task}
|
|
109
|
+
|
|
110
|
+
**Context:** A developer needs verification that this frontend is working correctly. Your assessment helps them catch issues before users do.
|
|
111
|
+
|
|
112
|
+
**Available tools:**
|
|
113
|
+
- navigate_page: Go to the URL
|
|
114
|
+
- take_screenshot: See the visual state (use this to understand what the page looks like)
|
|
115
|
+
- take_snapshot: Get element references for clicking/filling
|
|
116
|
+
- list_console_messages: Check for JavaScript errors
|
|
117
|
+
- click, fill, fill_form: Interact with elements
|
|
118
|
+
|
|
119
|
+
**Success criteria:** Provide a clear QA report covering visual state, console errors, task completion, and overall pass/fail assessment.
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
// Spawn sub-agent with chrome-devtools MCP access
|
|
123
|
+
const result = query({
|
|
124
|
+
prompt: taskPrompt,
|
|
125
|
+
options: {
|
|
126
|
+
model: "claude-opus-4-5",
|
|
127
|
+
cwd: process.cwd(),
|
|
128
|
+
systemPrompt: QA_SYSTEM_PROMPT,
|
|
129
|
+
mcpServers: {
|
|
130
|
+
"chrome-devtools": {
|
|
131
|
+
command: "npx",
|
|
132
|
+
args: ["chrome-devtools-mcp@latest", "--headless"],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
allowedTools: [
|
|
136
|
+
// Navigation
|
|
137
|
+
"mcp__chrome-devtools__navigate_page",
|
|
138
|
+
"mcp__chrome-devtools__new_page",
|
|
139
|
+
"mcp__chrome-devtools__list_pages",
|
|
140
|
+
"mcp__chrome-devtools__select_page",
|
|
141
|
+
"mcp__chrome-devtools__wait_for",
|
|
142
|
+
// Inspection
|
|
143
|
+
"mcp__chrome-devtools__take_screenshot",
|
|
144
|
+
"mcp__chrome-devtools__take_snapshot",
|
|
145
|
+
"mcp__chrome-devtools__list_console_messages",
|
|
146
|
+
"mcp__chrome-devtools__get_console_message",
|
|
147
|
+
// Interaction
|
|
148
|
+
"mcp__chrome-devtools__click",
|
|
149
|
+
"mcp__chrome-devtools__fill",
|
|
150
|
+
"mcp__chrome-devtools__fill_form",
|
|
151
|
+
"mcp__chrome-devtools__hover",
|
|
152
|
+
"mcp__chrome-devtools__press_key",
|
|
153
|
+
"mcp__chrome-devtools__handle_dialog",
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Collect the agent's final response
|
|
159
|
+
let report = "";
|
|
160
|
+
let totalCost = 0;
|
|
161
|
+
|
|
162
|
+
for await (const message of result) {
|
|
163
|
+
// Log progress to stderr (visible in server logs, not returned to client)
|
|
164
|
+
if (message.type === "system" && message.subtype === "init") {
|
|
165
|
+
console.error(`[QA Server] Agent initialized with tools: ${message.tools.length} tools`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (message.type === "assistant") {
|
|
169
|
+
// Extract text responses from assistant messages
|
|
170
|
+
const textBlocks = message.message.content.filter(
|
|
171
|
+
(c: any) => c.type === "text"
|
|
172
|
+
);
|
|
173
|
+
for (const block of textBlocks) {
|
|
174
|
+
report = block.text; // Keep latest text as the report
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Log tool calls
|
|
178
|
+
const toolCalls = message.message.content.filter(
|
|
179
|
+
(c: any) => c.type === "tool_use"
|
|
180
|
+
);
|
|
181
|
+
for (const tool of toolCalls) {
|
|
182
|
+
console.error(`[QA Server] Tool call: ${tool.name}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (message.type === "result") {
|
|
187
|
+
totalCost = message.total_cost_usd;
|
|
188
|
+
console.error(`[QA Server] Completed. Cost: $${totalCost.toFixed(4)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Return the QA report
|
|
193
|
+
return {
|
|
194
|
+
content: [
|
|
195
|
+
{
|
|
196
|
+
type: "text",
|
|
197
|
+
text: report || "No report generated. The agent may have encountered an issue.",
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
} catch (error) {
|
|
202
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
203
|
+
console.error(`[QA Server] Error: ${errorMessage}`);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
content: [
|
|
207
|
+
{
|
|
208
|
+
type: "text",
|
|
209
|
+
text: `QA inspection failed: ${errorMessage}`,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
isError: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Start the server
|
|
218
|
+
async function main() {
|
|
219
|
+
const transport = new StdioServerTransport();
|
|
220
|
+
await server.connect(transport);
|
|
221
|
+
console.error("[QA Server] Started and ready for connections");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
main().catch((error) => {
|
|
225
|
+
console.error("[QA Server] Fatal error:", error);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"resolveJsonModule": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|
|
@@ -8,94 +8,14 @@
|
|
|
8
8
|
* - Line 1: Git branch + status
|
|
9
9
|
* - Line 2: Context window usage bar with percentage and tokens
|
|
10
10
|
*
|
|
11
|
-
* Input: JSON on stdin with
|
|
11
|
+
* Input: JSON on stdin with context_window data
|
|
12
12
|
* Output: Formatted status to stdout
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
const {
|
|
15
|
+
const { readFileSync, readdirSync, statSync } = require('fs');
|
|
16
16
|
const { join, basename } = require('path');
|
|
17
17
|
const { execSync } = require('child_process');
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
* Get model token limit based on model ID
|
|
21
|
-
*/
|
|
22
|
-
function getModelTokenLimit(modelId) {
|
|
23
|
-
// Claude model context windows
|
|
24
|
-
if (modelId.includes('claude-3-5-sonnet') || modelId.includes('claude-sonnet-4')) {
|
|
25
|
-
return 200000;
|
|
26
|
-
}
|
|
27
|
-
if (modelId.includes('claude-3-opus')) {
|
|
28
|
-
return 200000;
|
|
29
|
-
}
|
|
30
|
-
if (modelId.includes('claude-3-haiku')) {
|
|
31
|
-
return 200000;
|
|
32
|
-
}
|
|
33
|
-
// Default fallback
|
|
34
|
-
return 200000;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Calculate context window usage from transcript
|
|
39
|
-
* Uses MOST RECENT message only (each message already reports full context)
|
|
40
|
-
*/
|
|
41
|
-
function calculateTokenUsage(transcriptPath) {
|
|
42
|
-
try {
|
|
43
|
-
if (!existsSync(transcriptPath)) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const content = readFileSync(transcriptPath, 'utf-8');
|
|
48
|
-
const lines = content.trim().split('\n');
|
|
49
|
-
|
|
50
|
-
// Parse all messages
|
|
51
|
-
const messages = [];
|
|
52
|
-
for (const line of lines) {
|
|
53
|
-
if (!line.trim()) continue;
|
|
54
|
-
try {
|
|
55
|
-
messages.push(JSON.parse(line));
|
|
56
|
-
} catch {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Find most recent main-chain message with usage data
|
|
62
|
-
const mainChainMessages = messages.filter((m) => m.isSidechain === false);
|
|
63
|
-
if (mainChainMessages.length === 0) {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Sort by timestamp descending (most recent first)
|
|
68
|
-
mainChainMessages.sort(
|
|
69
|
-
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
// Find first message with usage data
|
|
73
|
-
let contextLength = 0;
|
|
74
|
-
let modelId = '';
|
|
75
|
-
|
|
76
|
-
for (const msg of mainChainMessages) {
|
|
77
|
-
if (msg.message?.usage) {
|
|
78
|
-
const usage = msg.message.usage;
|
|
79
|
-
// Context = input + cache_read + cache_creation
|
|
80
|
-
// (each message already reports FULL context at that point)
|
|
81
|
-
contextLength =
|
|
82
|
-
(usage.input_tokens || 0) +
|
|
83
|
-
(usage.cache_read_input_tokens || 0) +
|
|
84
|
-
(usage.cache_creation_input_tokens || 0);
|
|
85
|
-
modelId = msg.message.model || '';
|
|
86
|
-
break; // Stop after most recent usage!
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const limit = getModelTokenLimit(modelId);
|
|
91
|
-
const percentage = Math.min(100, Math.round((contextLength / limit) * 100));
|
|
92
|
-
|
|
93
|
-
return { used: contextLength, limit, percentage };
|
|
94
|
-
} catch {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
19
|
/**
|
|
100
20
|
* Format number as K (e.g., 84000 -> "084K")
|
|
101
21
|
*/
|
|
@@ -269,10 +189,20 @@ function main() {
|
|
|
269
189
|
const DIM_GREY = '\x1b[38;5;245m';
|
|
270
190
|
const RESET = '\x1b[0m';
|
|
271
191
|
|
|
272
|
-
// Get token usage from
|
|
192
|
+
// Get token usage from context_window (available in Claude Code v2.0.70+)
|
|
273
193
|
let tokenData = null;
|
|
274
|
-
if (data.
|
|
275
|
-
|
|
194
|
+
if (data.context_window?.current_usage) {
|
|
195
|
+
const usage = data.context_window.current_usage;
|
|
196
|
+
const used =
|
|
197
|
+
(usage.input_tokens || 0) +
|
|
198
|
+
(usage.cache_read_input_tokens || 0) +
|
|
199
|
+
(usage.cache_creation_input_tokens || 0);
|
|
200
|
+
const limit = data.context_window.context_window_size || 200000;
|
|
201
|
+
tokenData = {
|
|
202
|
+
used,
|
|
203
|
+
limit,
|
|
204
|
+
percentage: Math.min(100, Math.round((used / limit) * 100)),
|
|
205
|
+
};
|
|
276
206
|
}
|
|
277
207
|
|
|
278
208
|
// Generate context display
|
|
@@ -81,6 +81,27 @@ After writing SKILL.md, ask: **"If Claude reads this right now, does it know wha
|
|
|
81
81
|
|
|
82
82
|
If the answer is "it would understand what the skill is about" but not "it would know what to do," rewrite it.
|
|
83
83
|
|
|
84
|
+
### Agent Prompts Are Different
|
|
85
|
+
|
|
86
|
+
The red flags above apply to SKILL.md files, not agent prompts. Agent prompts and skills have different characteristics:
|
|
87
|
+
|
|
88
|
+
| Aspect | Agent Prompt | SKILL.md |
|
|
89
|
+
|--------|--------------|----------|
|
|
90
|
+
| When read | Once per spawn | Repeatedly during session |
|
|
91
|
+
| Purpose | Provide context for focused task | Route to workflows |
|
|
92
|
+
| WHY/WHO/SUCCESS | Appropriate—crucial context | Red flag—wastes tokens |
|
|
93
|
+
| Lifespan | Single invocation | Entire user session |
|
|
94
|
+
|
|
95
|
+
Agent prompts benefit from a Purpose section with WHY/WHO/SUCCESS because:
|
|
96
|
+
- Agents spawn for a single focused task and need full context upfront
|
|
97
|
+
- The context is read once, not repeatedly
|
|
98
|
+
- Understanding purpose helps agents make better decisions
|
|
99
|
+
|
|
100
|
+
Skills should route to actions immediately because:
|
|
101
|
+
- Users interact with skills throughout a session
|
|
102
|
+
- Meta-descriptions consume tokens on every activation
|
|
103
|
+
- Instructions are more valuable than context in repeated interactions
|
|
104
|
+
|
|
84
105
|
## Progressive Disclosure
|
|
85
106
|
|
|
86
107
|
### The Problem Without It
|
|
@@ -129,6 +129,11 @@ function getPlanInfo(planDir, slug) {
|
|
|
129
129
|
const taskCounts = countTasks(manifest);
|
|
130
130
|
if (taskCounts) {
|
|
131
131
|
planInfo.tasks = taskCounts;
|
|
132
|
+
|
|
133
|
+
// Derive completion status: if all tasks are completed, the plan is completed
|
|
134
|
+
if (taskCounts.total > 0 && taskCounts.completed === taskCounts.total) {
|
|
135
|
+
planInfo.status = 'completed';
|
|
136
|
+
}
|
|
132
137
|
}
|
|
133
138
|
}
|
|
134
139
|
|