promptup-plugin 0.1.1

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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/bin/install.cjs +306 -0
  4. package/bin/promptup-plugin +8 -0
  5. package/dist/config.d.ts +40 -0
  6. package/dist/config.js +123 -0
  7. package/dist/db.d.ts +35 -0
  8. package/dist/db.js +327 -0
  9. package/dist/decision-detector.d.ts +11 -0
  10. package/dist/decision-detector.js +47 -0
  11. package/dist/evaluator.d.ts +10 -0
  12. package/dist/evaluator.js +844 -0
  13. package/dist/git-activity-extractor.d.ts +35 -0
  14. package/dist/git-activity-extractor.js +167 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.js +54 -0
  17. package/dist/pr-report-generator.d.ts +20 -0
  18. package/dist/pr-report-generator.js +421 -0
  19. package/dist/shared/decision-classifier.d.ts +60 -0
  20. package/dist/shared/decision-classifier.js +385 -0
  21. package/dist/shared/decision-score.d.ts +7 -0
  22. package/dist/shared/decision-score.js +31 -0
  23. package/dist/shared/dimensions.d.ts +43 -0
  24. package/dist/shared/dimensions.js +361 -0
  25. package/dist/shared/scoring.d.ts +89 -0
  26. package/dist/shared/scoring.js +161 -0
  27. package/dist/shared/types.d.ts +108 -0
  28. package/dist/shared/types.js +9 -0
  29. package/dist/tools.d.ts +30 -0
  30. package/dist/tools.js +456 -0
  31. package/dist/transcript-parser.d.ts +36 -0
  32. package/dist/transcript-parser.js +201 -0
  33. package/hooks/auto-eval.sh +44 -0
  34. package/hooks/check-update.sh +26 -0
  35. package/hooks/debug-hook.sh +3 -0
  36. package/hooks/hooks.json +36 -0
  37. package/hooks/render-eval.sh +137 -0
  38. package/package.json +60 -0
  39. package/skills/eval/SKILL.md +12 -0
  40. package/skills/pr-report/SKILL.md +37 -0
  41. package/skills/status/SKILL.md +28 -0
  42. package/statusline.sh +46 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Parser for Claude Code JSONL transcript files.
3
+ *
4
+ * Fully self-contained — no imports from @promptup/shared or any workspace package.
5
+ * Reads Claude Code JSONL files and produces MessageRow[] arrays.
6
+ *
7
+ * Claude Code JSONL format: each line is a JSON object with a `type` field:
8
+ * - "user" — user prompt (message.content: string | ContentBlock[])
9
+ * - "assistant" — model reply (message.content: ContentBlock[], message.usage, message.model)
10
+ * - "progress" — tool progress (filtered out)
11
+ * - "system" — system event (filtered out)
12
+ * - "result" — final result (filtered out)
13
+ * - "file_history_snapshot" — file state (filtered out)
14
+ */
15
+ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
16
+ import { join, basename } from 'node:path';
17
+ import { homedir } from 'node:os';
18
+ import { ulid } from 'ulid';
19
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
20
+ function isTextBlock(block) {
21
+ return block.type === 'text' && typeof block.text === 'string';
22
+ }
23
+ function isToolUseBlock(block) {
24
+ return block.type === 'tool_use' && typeof block.name === 'string';
25
+ }
26
+ /**
27
+ * Extract human-readable text from a message's content field.
28
+ */
29
+ function extractText(content) {
30
+ if (typeof content === 'string')
31
+ return content;
32
+ if (!Array.isArray(content))
33
+ return '';
34
+ const parts = [];
35
+ for (const block of content) {
36
+ if (isTextBlock(block)) {
37
+ parts.push(block.text);
38
+ }
39
+ else if (block.type === 'tool_result') {
40
+ const tr = block;
41
+ if (typeof tr.content === 'string') {
42
+ parts.push(`[tool_result] ${tr.content}`);
43
+ }
44
+ }
45
+ }
46
+ return parts.join('\n');
47
+ }
48
+ /**
49
+ * Extract tool_use blocks from content, returning a JSON-serializable array.
50
+ */
51
+ function extractToolUses(content) {
52
+ if (!Array.isArray(content))
53
+ return null;
54
+ const tools = [];
55
+ for (const block of content) {
56
+ if (isToolUseBlock(block)) {
57
+ tools.push({ name: block.name, input: block.input });
58
+ }
59
+ }
60
+ return tools.length > 0 ? JSON.stringify(tools) : null;
61
+ }
62
+ // ─── Main parser ─────────────────────────────────────────────────────────────
63
+ /**
64
+ * Parse a Claude Code JSONL transcript file into MessageRow[].
65
+ *
66
+ * Reads the file, splits by newlines, parses each JSON line, filters to
67
+ * 'user' and 'assistant' types, extracts text content, tool uses, token
68
+ * usage, and model name. Assigns 0-indexed sequence numbers and generates
69
+ * ULID-based IDs.
70
+ *
71
+ * Malformed lines are silently skipped.
72
+ */
73
+ export function parseTranscript(filePath) {
74
+ const raw = readFileSync(filePath, 'utf-8');
75
+ const lines = raw.split('\n');
76
+ const messages = [];
77
+ for (const line of lines) {
78
+ const trimmed = line.trim();
79
+ if (!trimmed)
80
+ continue;
81
+ try {
82
+ const parsed = JSON.parse(trimmed);
83
+ const type = parsed.type;
84
+ if (type !== 'user' && type !== 'assistant')
85
+ continue;
86
+ if (!parsed.message || typeof parsed.message !== 'object')
87
+ continue;
88
+ messages.push(parsed);
89
+ }
90
+ catch {
91
+ // Skip malformed lines
92
+ }
93
+ }
94
+ // Derive a session ID from the filename (strip .jsonl extension)
95
+ const sessionId = basename(filePath, '.jsonl');
96
+ const rows = [];
97
+ for (let i = 0; i < messages.length; i++) {
98
+ const msg = messages[i];
99
+ const id = ulid();
100
+ let role = msg.type;
101
+ const timestamp = msg.timestamp ?? new Date().toISOString();
102
+ let content;
103
+ let toolUses = null;
104
+ let tokensIn = 0;
105
+ let tokensOut = 0;
106
+ let model = null;
107
+ if (msg.type === 'user') {
108
+ content = extractText(msg.message.content);
109
+ // Tool result messages have content blocks that are all tool_result type.
110
+ // Keep them (they're useful for decision detection) but mark role as 'tool_result'
111
+ // so the report can distinguish developer prompts from tool outputs.
112
+ const msgContent = msg.message.content;
113
+ if (Array.isArray(msgContent) && msgContent.length > 0 && msgContent.every(b => b.type === 'tool_result')) {
114
+ role = 'tool_result';
115
+ }
116
+ }
117
+ else {
118
+ // assistant
119
+ const assistantContent = msg.message.content;
120
+ content = extractText(assistantContent);
121
+ toolUses = extractToolUses(assistantContent);
122
+ if (msg.message.usage) {
123
+ tokensIn = msg.message.usage.input_tokens ?? 0;
124
+ tokensOut = msg.message.usage.output_tokens ?? 0;
125
+ }
126
+ model = msg.message.model ?? null;
127
+ }
128
+ rows.push({
129
+ id,
130
+ session_id: sessionId,
131
+ role,
132
+ content,
133
+ tool_uses: toolUses,
134
+ sequence_number: i,
135
+ tokens_in: tokensIn,
136
+ tokens_out: tokensOut,
137
+ model,
138
+ created_at: timestamp,
139
+ });
140
+ }
141
+ return rows;
142
+ }
143
+ // ─── Find latest transcript ──────────────────────────────────────────────────
144
+ /**
145
+ * Find the most recent JSONL transcript file from Claude Code's project directory.
146
+ * Looks in ~/.claude/projects/ for the most recently modified .jsonl file.
147
+ *
148
+ * Walks the directory tree up to 3 levels deep:
149
+ * ~/.claude/projects/<project-hash>/sessions/<session-id>.jsonl
150
+ *
151
+ * Returns the absolute path to the most recently modified file, or null if none found.
152
+ */
153
+ export function findLatestTranscript() {
154
+ const projectsDir = join(homedir(), '.claude', 'projects');
155
+ if (!existsSync(projectsDir))
156
+ return null;
157
+ let latestPath = null;
158
+ let latestMtime = 0;
159
+ try {
160
+ const projectDirs = readdirSync(projectsDir, { withFileTypes: true });
161
+ for (const projectEntry of projectDirs) {
162
+ if (!projectEntry.isDirectory())
163
+ continue;
164
+ const projectPath = join(projectsDir, projectEntry.name);
165
+ // Look for .jsonl files directly in the project dir
166
+ scanForJsonl(projectPath);
167
+ // Look in a sessions/ subdirectory
168
+ const sessionsDir = join(projectPath, 'sessions');
169
+ if (existsSync(sessionsDir)) {
170
+ scanForJsonl(sessionsDir);
171
+ }
172
+ }
173
+ }
174
+ catch {
175
+ // Permissions error or similar — return what we have
176
+ }
177
+ function scanForJsonl(dir) {
178
+ try {
179
+ const entries = readdirSync(dir, { withFileTypes: true });
180
+ for (const entry of entries) {
181
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
182
+ continue;
183
+ const fullPath = join(dir, entry.name);
184
+ try {
185
+ const stat = statSync(fullPath);
186
+ if (stat.mtimeMs > latestMtime) {
187
+ latestMtime = stat.mtimeMs;
188
+ latestPath = fullPath;
189
+ }
190
+ }
191
+ catch {
192
+ // Skip files we can't stat
193
+ }
194
+ }
195
+ }
196
+ catch {
197
+ // Skip directories we can't read
198
+ }
199
+ }
200
+ return latestPath;
201
+ }
@@ -0,0 +1,44 @@
1
+ #!/bin/bash
2
+ # UserPromptSubmit hook: counts prompts, triggers background eval per config
3
+ PLUGIN_DIR="${CLAUDE_PLUGIN_ROOT:-$HOME/.promptup/plugin}"
4
+ DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.promptup}"
5
+ COUNT_FILE="$DATA_DIR/prompt-count"
6
+ CONFIG_FILE="$DATA_DIR/config.json"
7
+
8
+ mkdir -p "$DATA_DIR"
9
+
10
+ # Read config — check if auto_trigger is enabled
11
+ TRIGGER=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('evaluation',{}).get('auto_trigger','off'))" 2>/dev/null || echo "off")
12
+ if [ "$TRIGGER" = "off" ]; then
13
+ exit 0
14
+ fi
15
+
16
+ INTERVAL=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('evaluation',{}).get('interval',10))" 2>/dev/null || echo "10")
17
+
18
+ COUNT=$(cat "$COUNT_FILE" 2>/dev/null || echo 0)
19
+ COUNT=$((COUNT + 1))
20
+ echo $COUNT > "$COUNT_FILE"
21
+
22
+ if [ "$TRIGGER" = "prompt_count" ] && [ "$COUNT" -ge "$INTERVAL" ]; then
23
+ echo 0 > "$COUNT_FILE"
24
+ nohup node -e "
25
+ import { initDatabase, insertSession, insertMessages, getSession } from '$PLUGIN_DIR/dist/db.js';
26
+ import { parseTranscript, findLatestTranscript } from '$PLUGIN_DIR/dist/transcript-parser.js';
27
+ import { evaluateSession } from '$PLUGIN_DIR/dist/evaluator.js';
28
+ import { ulid } from 'ulid';
29
+ initDatabase();
30
+ const tp = findLatestTranscript();
31
+ if (!tp) process.exit(0);
32
+ const msgs = parseTranscript(tp);
33
+ if (msgs.length < 3) process.exit(0);
34
+ const sid = msgs[0]?.session_id || ulid();
35
+ if (!getSession(sid)) {
36
+ insertSession({ id: sid, project_path: process.cwd(), transcript_path: tp, status: 'active', message_count: msgs.length, started_at: msgs[0].created_at, ended_at: msgs[msgs.length-1].created_at, created_at: new Date().toISOString() });
37
+ }
38
+ for (const m of msgs) m.session_id = sid;
39
+ insertMessages(msgs);
40
+ await evaluateSession(sid, msgs, 'prompt_count');
41
+ " > /dev/null 2>&1 &
42
+ fi
43
+
44
+ exit 0
@@ -0,0 +1,26 @@
1
+ #!/bin/bash
2
+ # SessionStart hook: checks for plugin update in background
3
+ PLUGIN_DIR="${CLAUDE_PLUGIN_ROOT:-$HOME/.promptup/plugin}"
4
+ DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.promptup}"
5
+ UPDATE_FILE="$DATA_DIR/update-available"
6
+
7
+ mkdir -p "$DATA_DIR"
8
+
9
+ # Get local version
10
+ LOCAL_VER=$(node -e "console.log(require('$PLUGIN_DIR/package.json').version)" 2>/dev/null || echo "0.0.0")
11
+
12
+ # Check remote version (npm registry)
13
+ REMOTE_VER=$(npm view @promptup/plugin version 2>/dev/null || echo "")
14
+
15
+ if [ -z "$REMOTE_VER" ]; then
16
+ # Not published yet or no network — try GitHub
17
+ REMOTE_VER=$(curl -sf "https://raw.githubusercontent.com/promptup/claude-plugin/main/package.json" 2>/dev/null | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{console.log(JSON.parse(d).version)}catch{}})" 2>/dev/null || echo "")
18
+ fi
19
+
20
+ if [ -n "$REMOTE_VER" ] && [ "$REMOTE_VER" != "$LOCAL_VER" ]; then
21
+ echo "$REMOTE_VER" > "$UPDATE_FILE"
22
+ else
23
+ rm -f "$UPDATE_FILE"
24
+ fi
25
+
26
+ exit 0
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+ # Debug: capture PostToolUse input to see the actual format
3
+ cat > ~/.promptup/last-hook-input.json
@@ -0,0 +1,36 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "",
6
+ "hooks": [{
7
+ "type": "command",
8
+ "command": "cat >> \"${CLAUDE_PLUGIN_DATA:-$HOME/.promptup}/tool-events.jsonl\"",
9
+ "async": true
10
+ }]
11
+ },
12
+ {
13
+ "matcher": "mcp__promptup__evaluate_session",
14
+ "hooks": [{
15
+ "type": "command",
16
+ "command": "bash \"${CLAUDE_PLUGIN_ROOT:-$HOME/.promptup/plugin}/hooks/render-eval.sh\""
17
+ }]
18
+ },
19
+ {
20
+ "matcher": "mcp__promptup__generate_pr_report",
21
+ "hooks": [{
22
+ "type": "command",
23
+ "command": "bash \"${CLAUDE_PLUGIN_ROOT:-$HOME/.promptup/plugin}/hooks/render-eval.sh\""
24
+ }]
25
+ }
26
+ ],
27
+ "Stop": [{
28
+ "matcher": "",
29
+ "hooks": [{
30
+ "type": "command",
31
+ "command": "cat > \"${CLAUDE_PLUGIN_DATA:-$HOME/.promptup}/session-end.json\"",
32
+ "async": true
33
+ }]
34
+ }]
35
+ }
36
+ }
@@ -0,0 +1,137 @@
1
+ #!/bin/bash
2
+ # PostToolUse hook: renders eval results as a formatted table directly to stderr
3
+ # Triggered after mcp__promptup__evaluate_session completes
4
+ # Reads hook JSON from stdin, extracts the tool output text, renders table
5
+
6
+ INPUT=$(cat)
7
+
8
+ # Extract the tool output text from the hook JSON
9
+ # The hook input has: { tool_name, tool_input, tool_output: { content: [{ text: "..." }] } }
10
+ TEXT=$(echo "$INPUT" | jq -r '.tool_output.content[0].text // empty' 2>/dev/null)
11
+
12
+ if [ -z "$TEXT" ]; then
13
+ exit 0
14
+ fi
15
+
16
+ # Parse the markdown table from the eval output and render it with box drawing
17
+ # Extract lines between the table header and the next blank line
18
+ echo "$TEXT" | awk '
19
+ BEGIN {
20
+ # Colors
21
+ RED="\033[31m"
22
+ YEL="\033[33m"
23
+ GRN="\033[32m"
24
+ BLD="\033[1m"
25
+ DIM="\033[2m"
26
+ RST="\033[0m"
27
+ found_header=0
28
+ found_table=0
29
+ }
30
+
31
+ # Hero composite score line
32
+ /^### Composite Score:/ {
33
+ gsub(/^### /, "")
34
+ gsub(/\*\*/, "")
35
+ printf "\n" > "/dev/stderr"
36
+ printf " %s%s%s\n", BLD, $0, RST > "/dev/stderr"
37
+ next
38
+ }
39
+
40
+ # Progress bar line (emoji squares)
41
+ /^🟩|^🟨|^🟥/ {
42
+ printf " %s\n", $0 > "/dev/stderr"
43
+ printf "\n" > "/dev/stderr"
44
+ next
45
+ }
46
+
47
+ # Table header
48
+ /^\| Dimension \| Score \| Why \|/ {
49
+ printf " %s┌───────────────────────────┬──────────────────────────┬────────────────────────────────────────────────────────────────┐%s\n", DIM, RST > "/dev/stderr"
50
+ printf " %s│ %-25s │ %-24s │ %-62s │%s\n", BLD, "Dimension", "Score", "Why", RST > "/dev/stderr"
51
+ printf " %s├───────────────────────────┼──────────────────────────┼────────────────────────────────────────────────────────────────┤%s\n", DIM, RST > "/dev/stderr"
52
+ found_table=1
53
+ next
54
+ }
55
+
56
+ # Table separator (skip)
57
+ /^\|---/ { next }
58
+
59
+ # Table rows
60
+ found_table && /^\|/ {
61
+ # Split by |
62
+ n = split($0, cols, "|")
63
+ if (n >= 4) {
64
+ dim = cols[2]
65
+ score_col = cols[3]
66
+ why = cols[4]
67
+
68
+ # Trim whitespace
69
+ gsub(/^[ \t]+|[ \t]+$/, "", dim)
70
+ gsub(/^[ \t]+|[ \t]+$/, "", score_col)
71
+ gsub(/^[ \t]+|[ \t]+$/, "", why)
72
+
73
+ # Extract numeric score from score column
74
+ match(score_col, /[0-9]+/)
75
+ score = substr(score_col, RSTART, RLENGTH) + 0
76
+
77
+ # Color based on score
78
+ if (score >= 70) color = GRN
79
+ else if (score >= 40) color = YEL
80
+ else color = RED
81
+
82
+ # Truncate why to fit
83
+ if (length(why) > 62) why = substr(why, 1, 59) "..."
84
+
85
+ printf " │ %-25s │ %s%-24s%s │ %s%-62s%s │\n", dim, color, score_col, RST, DIM, why, RST > "/dev/stderr"
86
+ }
87
+ next
88
+ }
89
+
90
+ # End of table (blank line after table)
91
+ found_table && /^$/ {
92
+ printf " %s└───────────────────────────┴──────────────────────────┴────────────────────────────────────────────────────────────────┘%s\n", DIM, RST > "/dev/stderr"
93
+ found_table=0
94
+ next
95
+ }
96
+
97
+ # Developer prompts line
98
+ /^Developer prompts:/ {
99
+ gsub(/\*\*/, "")
100
+ printf "\n %s%s%s\n", DIM, $0, RST > "/dev/stderr"
101
+ next
102
+ }
103
+
104
+ # Decisions header
105
+ /^### Decisions/ {
106
+ printf "\n %s%sDecisions%s\n", BLD, "", RST > "/dev/stderr"
107
+ next
108
+ }
109
+
110
+ # Decision lines (emoji + text)
111
+ /^🔀|^🚫|^✅|^✏️|^📐|^👍/ {
112
+ gsub(/\*\*/, "\033[1m")
113
+ printf " %s\n", $0 > "/dev/stderr"
114
+ next
115
+ }
116
+
117
+ # Routine decisions note
118
+ /^\*\+/ {
119
+ gsub(/\*/, "")
120
+ printf " %s%s%s\n", DIM, $0, RST > "/dev/stderr"
121
+ next
122
+ }
123
+
124
+ # Recommendations header
125
+ /^### Recommendations/ {
126
+ printf "\n %s%sRecommendations%s\n", BLD, "", RST > "/dev/stderr"
127
+ next
128
+ }
129
+
130
+ # Recommendation lines
131
+ /^🔴|^🟡|^🟢/ {
132
+ printf " %s\n", $0 > "/dev/stderr"
133
+ next
134
+ }
135
+ ' 2>&1
136
+
137
+ exit 0
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "promptup-plugin",
3
+ "version": "0.1.1",
4
+ "description": "AI coding skill evaluator for Claude Code — 11-dimension scoring, decision intelligence, PR reports",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "promptup-plugin": "bin/promptup-plugin"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "dist",
13
+ "hooks",
14
+ "skills",
15
+ "statusline.sh"
16
+ ],
17
+ "engines": {
18
+ "node": ">=20.0.0"
19
+ },
20
+ "keywords": [
21
+ "claude",
22
+ "claude-code",
23
+ "ai",
24
+ "evaluation",
25
+ "developer-tools",
26
+ "coding-skill",
27
+ "mcp",
28
+ "mcp-server",
29
+ "model-context-protocol",
30
+ "promptup",
31
+ "session-analysis",
32
+ "decision-quality",
33
+ "pr-report"
34
+ ],
35
+ "author": "gabikreal1",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/gabikreal1/promptup-plugin.git"
40
+ },
41
+ "homepage": "https://github.com/gabikreal1/promptup-plugin",
42
+ "bugs": {
43
+ "url": "https://github.com/gabikreal1/promptup-plugin/issues"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc",
47
+ "dev": "tsc --watch",
48
+ "prepublishOnly": "echo 'Ensure dist/ is built before publishing'"
49
+ },
50
+ "dependencies": {
51
+ "@modelcontextprotocol/sdk": "^1.12.0",
52
+ "better-sqlite3": "^11.0.0",
53
+ "ulid": "^2.3.0",
54
+ "zod": "^3.24.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/better-sqlite3": "^7.6.0",
58
+ "typescript": "^5.9.0"
59
+ }
60
+ }
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: eval
3
+ description: Evaluate the current coding session across 11 dimensions. Spawns an independent Claude analysis of your session transcript for unbiased scoring.
4
+ user-invocable: true
5
+ argument-hint: [session-id]
6
+ ---
7
+
8
+ Call `mcp__promptup__evaluate_session` with the session ID from `$ARGUMENTS` if provided, otherwise no arguments.
9
+
10
+ **CRITICAL: The tool returns a fully formatted markdown report. Output the ENTIRE tool response text verbatim in your reply — do NOT summarize, rephrase, or truncate any part of it. Include the full table, all progress bars, all decisions, and all recommendations exactly as returned. The user needs to see every line.**
11
+
12
+ If the evaluation fails, explain what happened and suggest running `/status`.
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: pr-report
3
+ description: Generate a Decision Quality Score (DQS) report for the current branch. Shows developer decisions made during AI-assisted coding, optionally posts to GitHub PR.
4
+ user-invocable: true
5
+ argument-hint: [--post] [--branch name]
6
+ ---
7
+
8
+ # /pr-report — PR Decision Report
9
+
10
+ Generate a decision quality report for the current git branch using PromptUp MCP tools.
11
+
12
+ ## Instructions
13
+
14
+ 1. Parse the user's arguments (`$ARGUMENTS`):
15
+ - If `--post` is present, set `post: true`
16
+ - If `--branch <name>` is present, set `branch` to that value
17
+ - Otherwise use defaults (current branch, no posting)
18
+
19
+ 2. Call the `mcp__promptup__generate_pr_report` tool with the parsed options.
20
+
21
+ 3. Display the returned **markdown report** directly — it contains:
22
+ - DQS score (Decision Quality Score, 0-100)
23
+ - Key decisions grouped by category (Architecture & Direction, Quality Gates, Scope Adjustments, Overrides)
24
+ - Summary table of decisions by type and signal level
25
+ - Commits list
26
+
27
+ 4. If `--post` was used and the report was posted to GitHub:
28
+ - Confirm: "Posted to PR #N"
29
+ - Show the PR URL
30
+
31
+ 5. If `--post` was used but posting failed:
32
+ - Explain why (no PR found, gh CLI not available, etc.)
33
+ - Suggest: install `gh` CLI, push branch first, create PR first
34
+
35
+ 6. If no sessions or decisions were found for the branch:
36
+ - Explain that PromptUp needs active session data
37
+ - Suggest running `/status` to check what's been tracked
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: status
3
+ description: Show PromptUp tracking status — recent sessions, evaluations, and decision counts.
4
+ user-invocable: true
5
+ ---
6
+
7
+ # /status — PromptUp Status
8
+
9
+ Show the current state of PromptUp tracking.
10
+
11
+ ## Instructions
12
+
13
+ 1. Call the `mcp__promptup__get_status` tool.
14
+
15
+ 2. Present the results as a clean summary:
16
+
17
+ **PromptUp Status**
18
+ - Sessions tracked: N
19
+ - Evaluations completed: N
20
+ - Decisions captured: N
21
+ - Latest session: [id] (started X ago)
22
+ - Latest eval score: X/100
23
+ - Tool events logged: N
24
+
25
+ 3. If no data exists yet, explain:
26
+ - PromptUp tracks sessions passively via hooks
27
+ - Run `/eval` after a coding session to generate scores
28
+ - Run `/pr-report` on a branch with commits to see decision analysis
package/statusline.sh ADDED
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ DB="${CLAUDE_PLUGIN_DATA:-$HOME/.promptup}/promptup.db"
3
+ DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.promptup}"
4
+ CONFIG_FILE="$DATA_DIR/config.json"
5
+ UPDATE_FILE="$DATA_DIR/update-available"
6
+
7
+ if [ ! -f "$DB" ]; then
8
+ echo "pupmeter: --"
9
+ exit 0
10
+ fi
11
+
12
+ SCORE=$(sqlite3 "$DB" "SELECT composite_score FROM evaluations ORDER BY created_at DESC LIMIT 1" 2>/dev/null)
13
+ SCORE_INT=$(printf "%.0f" "${SCORE:-0}")
14
+
15
+ FILLED=$((SCORE_INT / 10))
16
+ EMPTY=$((10 - FILLED))
17
+ BAR=""
18
+ for i in $(seq 1 $FILLED 2>/dev/null); do BAR="${BAR}█"; done
19
+ for i in $(seq 1 $EMPTY 2>/dev/null); do BAR="${BAR}░"; done
20
+
21
+ # Check for update
22
+ UPDATE=""
23
+ if [ -f "$UPDATE_FILE" ]; then
24
+ NEW_VER=$(cat "$UPDATE_FILE")
25
+ UPDATE=" │ ⬆ ${NEW_VER}"
26
+ fi
27
+
28
+ echo "pupmeter ${BAR} ${SCORE_INT}%${UPDATE}"
29
+
30
+ # Show recommendation if enabled
31
+ SHOW_REC=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('statusline',{}).get('show_recommendation',True))" 2>/dev/null || echo "True")
32
+ if [ "$SHOW_REC" = "True" ]; then
33
+ REC=$(sqlite3 "$DB" "SELECT recommendations FROM evaluations WHERE recommendations IS NOT NULL ORDER BY created_at DESC LIMIT 1" 2>/dev/null)
34
+ if [ -n "$REC" ]; then
35
+ TIP=$(echo "$REC" | python3 -c "
36
+ import sys,json
37
+ try:
38
+ r=json.load(sys.stdin)
39
+ if r: print(r[0].get('recommendation','')[:60])
40
+ except: pass
41
+ " 2>/dev/null)
42
+ if [ -n "$TIP" ]; then
43
+ echo "💡 ${TIP}"
44
+ fi
45
+ fi
46
+ fi