supipowers 0.3.0 → 0.5.0
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/package.json +1 -1
- package/skills/fix-pr/SKILL.md +99 -0
- package/skills/qa-strategy/SKILL.md +103 -21
- package/src/commands/fix-pr.ts +324 -0
- package/src/commands/qa.ts +232 -148
- package/src/commands/supi.ts +2 -1
- package/src/config/defaults.ts +1 -0
- package/src/config/schema.ts +1 -0
- package/src/fix-pr/config.ts +36 -0
- package/src/fix-pr/prompt-builder.ts +201 -0
- package/src/fix-pr/scripts/diff-comments.sh +33 -0
- package/src/fix-pr/scripts/fetch-pr-comments.sh +25 -0
- package/src/fix-pr/scripts/trigger-review.sh +36 -0
- package/src/fix-pr/scripts/wait-and-check.sh +37 -0
- package/src/fix-pr/types.ts +71 -0
- package/src/index.ts +2 -0
- package/src/qa/config.ts +43 -0
- package/src/qa/matrix.ts +84 -0
- package/src/qa/prompt-builder.ts +212 -0
- package/src/qa/scripts/detect-app-type.sh +68 -0
- package/src/qa/scripts/discover-routes.sh +143 -0
- package/src/qa/scripts/ensure-playwright.sh +38 -0
- package/src/qa/scripts/run-e2e-tests.sh +99 -0
- package/src/qa/scripts/start-dev-server.sh +46 -0
- package/src/qa/scripts/stop-dev-server.sh +36 -0
- package/src/qa/session.ts +39 -55
- package/src/qa/types.ts +97 -0
- package/src/storage/fix-pr-sessions.ts +59 -0
- package/src/storage/qa-sessions.ts +9 -9
- package/src/types.ts +1 -70
- package/src/qa/detector.ts +0 -61
- package/src/qa/phases/discovery.ts +0 -34
- package/src/qa/phases/execution.ts +0 -65
- package/src/qa/phases/matrix.ts +0 -41
- package/src/qa/phases/reporting.ts +0 -71
- package/src/qa/report.ts +0 -22
- package/src/qa/runner.ts +0 -46
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { FixPrConfig } from "./types.js";
|
|
2
|
+
import { buildReceivingReviewInstructions } from "../discipline/receiving-review.js";
|
|
3
|
+
|
|
4
|
+
export interface FixPrPromptOptions {
|
|
5
|
+
prNumber: number;
|
|
6
|
+
repo: string;
|
|
7
|
+
comments: string;
|
|
8
|
+
sessionDir: string;
|
|
9
|
+
scriptsDir: string;
|
|
10
|
+
config: FixPrConfig;
|
|
11
|
+
iteration: number;
|
|
12
|
+
skillContent: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildReplyInstructions(config: FixPrConfig): string {
|
|
16
|
+
const { commentPolicy, repo, } = config;
|
|
17
|
+
const replyCmd = `gh api repos/REPO/pulls/PR/comments/COMMENT_ID/replies -f body="..."`;
|
|
18
|
+
|
|
19
|
+
switch (commentPolicy) {
|
|
20
|
+
case "no-answer":
|
|
21
|
+
return [
|
|
22
|
+
"### Comment Replies",
|
|
23
|
+
"",
|
|
24
|
+
"Policy: **Do not reply** to any comments. Focus only on fixing the code.",
|
|
25
|
+
"Do not post any replies via gh api.",
|
|
26
|
+
].join("\n");
|
|
27
|
+
case "answer-all":
|
|
28
|
+
return [
|
|
29
|
+
"### Comment Replies",
|
|
30
|
+
"",
|
|
31
|
+
"Policy: **Answer all** comments — both accepted and rejected.",
|
|
32
|
+
"For each comment, post a reply explaining what was done or why it was rejected.",
|
|
33
|
+
`Use: \`${replyCmd}\``,
|
|
34
|
+
"Keep replies factual and technical. No performative agreement.",
|
|
35
|
+
].join("\n");
|
|
36
|
+
case "answer-selective":
|
|
37
|
+
return [
|
|
38
|
+
"### Comment Replies",
|
|
39
|
+
"",
|
|
40
|
+
"Policy: **Answer selectively** — only reply to comments you reject or where clarification adds value.",
|
|
41
|
+
"For ACCEPT: fix silently (the code change speaks for itself).",
|
|
42
|
+
"For REJECT: explain why with technical reasoning.",
|
|
43
|
+
`Use: \`${replyCmd}\``,
|
|
44
|
+
"Keep replies factual. No performative agreement.",
|
|
45
|
+
].join("\n");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildFixPrOrchestratorPrompt(options: FixPrPromptOptions): string {
|
|
50
|
+
const { prNumber, repo, comments, sessionDir, scriptsDir, config, iteration, skillContent } = options;
|
|
51
|
+
const { loop, models, reviewer } = config;
|
|
52
|
+
const maxIter = loop.maxIterations;
|
|
53
|
+
const delay = loop.delaySeconds;
|
|
54
|
+
|
|
55
|
+
const sections: string[] = [
|
|
56
|
+
"# PR Review Fix Orchestration",
|
|
57
|
+
"",
|
|
58
|
+
`You are the orchestrator for fixing PR #${prNumber} on \`${repo}\`.`,
|
|
59
|
+
"",
|
|
60
|
+
"## Session Context",
|
|
61
|
+
"",
|
|
62
|
+
`- Session dir: \`${sessionDir}\``,
|
|
63
|
+
`- Iteration: ${iteration} of ${maxIter}`,
|
|
64
|
+
`- Comment reply policy: ${config.commentPolicy}`,
|
|
65
|
+
`- Reviewer: ${reviewer.type}${reviewer.triggerMethod ? ` (trigger: ${reviewer.triggerMethod})` : ""}`,
|
|
66
|
+
"",
|
|
67
|
+
"## Review Comments to Process",
|
|
68
|
+
"",
|
|
69
|
+
"Each line is a JSON object with comment data:",
|
|
70
|
+
"",
|
|
71
|
+
"```jsonl",
|
|
72
|
+
comments,
|
|
73
|
+
"```",
|
|
74
|
+
"",
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// Embedded skill
|
|
78
|
+
if (skillContent) {
|
|
79
|
+
sections.push(
|
|
80
|
+
"## Assessment Methodology",
|
|
81
|
+
"",
|
|
82
|
+
skillContent,
|
|
83
|
+
"",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Receiving review discipline
|
|
88
|
+
sections.push(
|
|
89
|
+
"## Review Discipline",
|
|
90
|
+
"",
|
|
91
|
+
buildReceivingReviewInstructions(),
|
|
92
|
+
"",
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Step 1: Assess
|
|
96
|
+
sections.push(
|
|
97
|
+
"## Step 1: Assess Each Comment",
|
|
98
|
+
"",
|
|
99
|
+
"For each comment:",
|
|
100
|
+
"1. Read the actual code at the file and line referenced",
|
|
101
|
+
"2. Determine the verdict: **ACCEPT** / **REJECT** / **INVESTIGATE**",
|
|
102
|
+
"3. Check ripple effects — who calls this, what tests cover it",
|
|
103
|
+
"4. YAGNI check — does the reviewer's suggestion address a real problem?",
|
|
104
|
+
"",
|
|
105
|
+
"Record your assessment:",
|
|
106
|
+
"```",
|
|
107
|
+
"Comment #ID by @user on file:line",
|
|
108
|
+
"Verdict: ACCEPT | REJECT | INVESTIGATE",
|
|
109
|
+
"Reasoning: [1-2 sentences]",
|
|
110
|
+
"Ripple effects: [list or none]",
|
|
111
|
+
"Group: [group-id]",
|
|
112
|
+
"```",
|
|
113
|
+
"",
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Step 2: Group
|
|
117
|
+
sections.push(
|
|
118
|
+
"## Step 2: Group Comments",
|
|
119
|
+
"",
|
|
120
|
+
"Group accepted comments for parallel execution:",
|
|
121
|
+
"- Same file or tightly coupled files → same group",
|
|
122
|
+
"- Independent files/areas → separate groups",
|
|
123
|
+
"- Cosmetic vs functional → separate groups",
|
|
124
|
+
"",
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Step 3: Plan
|
|
128
|
+
sections.push(
|
|
129
|
+
"## Step 3: Plan Each Group",
|
|
130
|
+
"",
|
|
131
|
+
"For each group, create a fix plan:",
|
|
132
|
+
"- What changes are needed and why",
|
|
133
|
+
"- Which files to modify",
|
|
134
|
+
"- Expected ripple effects and how to handle them",
|
|
135
|
+
"- How to verify the fix (which tests to run)",
|
|
136
|
+
"",
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Step 4: Execute
|
|
140
|
+
sections.push(
|
|
141
|
+
"## Step 4: Execute Fixes",
|
|
142
|
+
"",
|
|
143
|
+
"For each group:",
|
|
144
|
+
"1. Make the code changes",
|
|
145
|
+
"2. Run relevant tests to verify",
|
|
146
|
+
"3. If tests fail, fix before moving on",
|
|
147
|
+
"",
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Step 5: Reply
|
|
151
|
+
sections.push(buildReplyInstructions(config), "");
|
|
152
|
+
|
|
153
|
+
// Step 6: Push and loop
|
|
154
|
+
sections.push(
|
|
155
|
+
"## Step 6: Push and Check for New Comments",
|
|
156
|
+
"",
|
|
157
|
+
'1. Stage and commit: `git add -A && git commit -m "fix: address PR review comments (iteration ' + iteration + ')"`',
|
|
158
|
+
"2. Push: `git push`",
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (reviewer.type !== "none" && reviewer.triggerMethod) {
|
|
162
|
+
sections.push(
|
|
163
|
+
`3. Trigger re-review: \`bash ${scriptsDir}/trigger-review.sh "${repo}" ${prNumber} "${reviewer.type}" "${reviewer.triggerMethod}"\``,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
sections.push(
|
|
168
|
+
`${reviewer.type !== "none" ? "4" : "3"}. Run the check script:`,
|
|
169
|
+
"```bash",
|
|
170
|
+
`bash ${scriptsDir}/wait-and-check.sh "${sessionDir}" ${delay} ${iteration + 1} "${repo}" ${prNumber}`,
|
|
171
|
+
"```",
|
|
172
|
+
`${reviewer.type !== "none" ? "5" : "4"}. Read the last line of output:`,
|
|
173
|
+
` - If \`hasNewComments: true\` and iteration < ${maxIter}: process the new comments (go back to Step 1)`,
|
|
174
|
+
` - If \`hasNewComments: false\` or iteration >= ${maxIter}: report done`,
|
|
175
|
+
"",
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Script paths reference
|
|
179
|
+
sections.push(
|
|
180
|
+
"## Script Paths",
|
|
181
|
+
"",
|
|
182
|
+
`- fetch-pr-comments.sh: \`${scriptsDir}/fetch-pr-comments.sh\``,
|
|
183
|
+
`- diff-comments.sh: \`${scriptsDir}/diff-comments.sh\``,
|
|
184
|
+
`- trigger-review.sh: \`${scriptsDir}/trigger-review.sh\``,
|
|
185
|
+
`- wait-and-check.sh: \`${scriptsDir}/wait-and-check.sh\``,
|
|
186
|
+
"",
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Model guidance
|
|
190
|
+
sections.push(
|
|
191
|
+
"## Model Guidance",
|
|
192
|
+
"",
|
|
193
|
+
`- **Orchestrator** (assessment, grouping): ${models.orchestrator.model} (${models.orchestrator.tier} tier) — thorough analysis`,
|
|
194
|
+
`- **Planner** (fix planning): ${models.planner.model} (${models.planner.tier} tier) — detailed planning`,
|
|
195
|
+
`- **Fixer** (code changes): ${models.fixer.model} (${models.fixer.tier} tier) — focused execution`,
|
|
196
|
+
"",
|
|
197
|
+
"These indicate the expected reasoning depth for each phase of work.",
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return sections.join("\n");
|
|
201
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Compares two JSONL comment snapshots, outputs only new/changed comments
|
|
3
|
+
# Usage: diff-comments.sh <prev_snapshot> <new_snapshot>
|
|
4
|
+
# Exit 0 if new comments found, exit 1 if identical
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
PREV="$1"
|
|
8
|
+
NEW="$2"
|
|
9
|
+
|
|
10
|
+
# If no previous snapshot, all comments are new
|
|
11
|
+
if [[ ! -f "$PREV" ]]; then
|
|
12
|
+
cat "$NEW"
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Build fingerprint: id + updatedAt for each comment
|
|
17
|
+
prev_fingerprints=$(jq -r '[.id, .updatedAt] | @tsv' "$PREV" 2>/dev/null | sort)
|
|
18
|
+
new_fingerprints=$(jq -r '[.id, .updatedAt] | @tsv' "$NEW" 2>/dev/null | sort)
|
|
19
|
+
|
|
20
|
+
# Find IDs that are new or changed
|
|
21
|
+
new_ids=$(comm -13 <(echo "$prev_fingerprints") <(echo "$new_fingerprints") | cut -f1)
|
|
22
|
+
|
|
23
|
+
if [[ -z "$new_ids" ]]; then
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Output the full comment objects for new/changed IDs
|
|
28
|
+
while IFS= read -r id; do
|
|
29
|
+
[[ -z "$id" ]] && continue
|
|
30
|
+
jq -c "select(.id == $id)" "$NEW"
|
|
31
|
+
done <<< "$new_ids"
|
|
32
|
+
|
|
33
|
+
exit 0
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Fetches all review comments for a PR, outputs JSONL
|
|
3
|
+
# Usage: fetch-pr-comments.sh <owner/repo> <pr_number> <output_file>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
REPO="$1"
|
|
7
|
+
PR="$2"
|
|
8
|
+
OUTPUT="$3"
|
|
9
|
+
|
|
10
|
+
# Ensure output directory exists
|
|
11
|
+
mkdir -p "$(dirname "$OUTPUT")"
|
|
12
|
+
|
|
13
|
+
# Fetch inline review comments (code-level)
|
|
14
|
+
gh api --paginate "repos/${REPO}/pulls/${PR}/comments" \
|
|
15
|
+
--jq '.[] | {id, path, line: .line, body, user: .user.login, createdAt: .created_at, updatedAt: .updated_at, inReplyToId: .in_reply_to_id, diffHunk: .diff_hunk, state: "COMMENTED"}' \
|
|
16
|
+
> "$OUTPUT" 2>/dev/null || true
|
|
17
|
+
|
|
18
|
+
# Fetch review-level comments (top-level reviews with body text)
|
|
19
|
+
gh api --paginate "repos/${REPO}/pulls/${PR}/reviews" \
|
|
20
|
+
--jq '.[] | select(.body != null and .body != "") | {id, path: null, line: null, body, user: .user.login, createdAt: .submitted_at, updatedAt: .submitted_at, inReplyToId: null, diffHunk: null, state}' \
|
|
21
|
+
>> "$OUTPUT" 2>/dev/null || true
|
|
22
|
+
|
|
23
|
+
# Output summary to stderr for caller
|
|
24
|
+
TOTAL=$(wc -l < "$OUTPUT" | tr -d ' ')
|
|
25
|
+
echo "{\"total\": ${TOTAL}}" >&2
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Triggers automated reviewer to re-review a PR
|
|
3
|
+
# Usage: trigger-review.sh <owner/repo> <pr_number> <reviewer_type> <trigger_method>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
REPO="$1"
|
|
7
|
+
PR="$2"
|
|
8
|
+
REVIEWER="$3"
|
|
9
|
+
METHOD="${4:-}"
|
|
10
|
+
|
|
11
|
+
case "$REVIEWER" in
|
|
12
|
+
coderabbit)
|
|
13
|
+
gh api "repos/${REPO}/issues/${PR}/comments" -f body="$METHOD" >/dev/null 2>&1
|
|
14
|
+
echo '{"triggered": true, "reviewer": "coderabbit"}'
|
|
15
|
+
;;
|
|
16
|
+
copilot)
|
|
17
|
+
if [[ -n "$METHOD" ]]; then
|
|
18
|
+
gh api "repos/${REPO}/issues/${PR}/comments" -f body="$METHOD" >/dev/null 2>&1
|
|
19
|
+
else
|
|
20
|
+
gh api "repos/${REPO}/pulls/${PR}/requested_reviewers" \
|
|
21
|
+
--method POST -f "reviewers[]=copilot" >/dev/null 2>&1 || true
|
|
22
|
+
fi
|
|
23
|
+
echo '{"triggered": true, "reviewer": "copilot"}'
|
|
24
|
+
;;
|
|
25
|
+
gemini)
|
|
26
|
+
gh api "repos/${REPO}/issues/${PR}/comments" -f body="$METHOD" >/dev/null 2>&1
|
|
27
|
+
echo '{"triggered": true, "reviewer": "gemini"}'
|
|
28
|
+
;;
|
|
29
|
+
none)
|
|
30
|
+
echo '{"triggered": false, "reviewer": "none"}'
|
|
31
|
+
;;
|
|
32
|
+
*)
|
|
33
|
+
echo '{"triggered": false, "error": "unknown reviewer type: '"$REVIEWER"'"}'
|
|
34
|
+
exit 1
|
|
35
|
+
;;
|
|
36
|
+
esac
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Waits for delay, fetches new PR comments, diffs against previous snapshot
|
|
3
|
+
# Usage: wait-and-check.sh <session_dir> <delay_seconds> <iteration> <owner/repo> <pr_number>
|
|
4
|
+
# Output: new comment lines + JSON summary on last line
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
SESSION_DIR="$1"
|
|
8
|
+
DELAY="$2"
|
|
9
|
+
ITERATION="$3"
|
|
10
|
+
REPO="$4"
|
|
11
|
+
PR="$5"
|
|
12
|
+
|
|
13
|
+
SNAPSHOTS_DIR="${SESSION_DIR}/snapshots"
|
|
14
|
+
PREV_ITERATION=$((ITERATION - 1))
|
|
15
|
+
PREV_SNAPSHOT="${SNAPSHOTS_DIR}/comments-${PREV_ITERATION}.jsonl"
|
|
16
|
+
NEW_SNAPSHOT="${SNAPSHOTS_DIR}/comments-${ITERATION}.jsonl"
|
|
17
|
+
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
19
|
+
|
|
20
|
+
# Wait for reviewer to process
|
|
21
|
+
echo "Waiting ${DELAY}s for reviewer to process changes..." >&2
|
|
22
|
+
sleep "$DELAY"
|
|
23
|
+
|
|
24
|
+
# Fetch new comments
|
|
25
|
+
echo "Fetching PR comments (iteration ${ITERATION})..." >&2
|
|
26
|
+
bash "${SCRIPT_DIR}/fetch-pr-comments.sh" "$REPO" "$PR" "$NEW_SNAPSHOT"
|
|
27
|
+
|
|
28
|
+
# Diff against previous
|
|
29
|
+
DIFF_OUTPUT=$(bash "${SCRIPT_DIR}/diff-comments.sh" "$PREV_SNAPSHOT" "$NEW_SNAPSHOT" 2>/dev/null) || true
|
|
30
|
+
|
|
31
|
+
if [[ -n "$DIFF_OUTPUT" ]]; then
|
|
32
|
+
DIFF_COUNT=$(echo "$DIFF_OUTPUT" | wc -l | tr -d ' ')
|
|
33
|
+
echo "$DIFF_OUTPUT"
|
|
34
|
+
echo "{\"hasNewComments\": true, \"count\": ${DIFF_COUNT}, \"iteration\": ${ITERATION}}"
|
|
35
|
+
else
|
|
36
|
+
echo "{\"hasNewComments\": false, \"count\": 0, \"iteration\": ${ITERATION}}"
|
|
37
|
+
fi
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Supported automated PR reviewers */
|
|
2
|
+
export type ReviewerType = "coderabbit" | "copilot" | "gemini" | "none";
|
|
3
|
+
|
|
4
|
+
/** How to handle comment replies */
|
|
5
|
+
export type CommentReplyPolicy = "answer-all" | "answer-selective" | "no-answer";
|
|
6
|
+
|
|
7
|
+
/** Model preference for a specific role */
|
|
8
|
+
export interface ModelPref {
|
|
9
|
+
provider: string;
|
|
10
|
+
model: string;
|
|
11
|
+
tier: "low" | "high";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Per-repo fix-pr configuration */
|
|
15
|
+
export interface FixPrConfig {
|
|
16
|
+
reviewer: {
|
|
17
|
+
type: ReviewerType;
|
|
18
|
+
triggerMethod: string | null;
|
|
19
|
+
};
|
|
20
|
+
commentPolicy: CommentReplyPolicy;
|
|
21
|
+
loop: {
|
|
22
|
+
delaySeconds: number;
|
|
23
|
+
maxIterations: number;
|
|
24
|
+
};
|
|
25
|
+
models: {
|
|
26
|
+
orchestrator: ModelPref;
|
|
27
|
+
planner: ModelPref;
|
|
28
|
+
fixer: ModelPref;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A PR review comment from GitHub API */
|
|
33
|
+
export interface PrComment {
|
|
34
|
+
id: number;
|
|
35
|
+
path: string | null;
|
|
36
|
+
line: number | null;
|
|
37
|
+
body: string;
|
|
38
|
+
user: string;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
updatedAt: string;
|
|
41
|
+
inReplyToId: number | null;
|
|
42
|
+
diffHunk: string | null;
|
|
43
|
+
state: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Assessment verdict for a single comment */
|
|
47
|
+
export type CommentVerdict = "accept" | "reject" | "investigate";
|
|
48
|
+
|
|
49
|
+
/** A group of related comments to fix together */
|
|
50
|
+
export interface FixGroup {
|
|
51
|
+
id: string;
|
|
52
|
+
commentIds: number[];
|
|
53
|
+
files: string[];
|
|
54
|
+
description: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Session status */
|
|
58
|
+
export type FixPrSessionStatus = "running" | "completed" | "failed";
|
|
59
|
+
|
|
60
|
+
/** Session ledger for a fix-pr run */
|
|
61
|
+
export interface FixPrSessionLedger {
|
|
62
|
+
id: string;
|
|
63
|
+
createdAt: string;
|
|
64
|
+
updatedAt: string;
|
|
65
|
+
prNumber: number;
|
|
66
|
+
repo: string;
|
|
67
|
+
status: FixPrSessionStatus;
|
|
68
|
+
iteration: number;
|
|
69
|
+
config: FixPrConfig;
|
|
70
|
+
commentsProcessed: number[];
|
|
71
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { registerReviewCommand } from "./commands/review.js";
|
|
|
12
12
|
import { registerQaCommand } from "./commands/qa.js";
|
|
13
13
|
import { registerReleaseCommand } from "./commands/release.js";
|
|
14
14
|
import { registerUpdateCommand, handleUpdate } from "./commands/update.js";
|
|
15
|
+
import { registerFixPrCommand } from "./commands/fix-pr.js";
|
|
15
16
|
|
|
16
17
|
// TUI-only commands — intercepted at the input level to prevent
|
|
17
18
|
// message submission and "Working..." indicator
|
|
@@ -43,6 +44,7 @@ export default function supipowers(pi: ExtensionAPI): void {
|
|
|
43
44
|
registerQaCommand(pi);
|
|
44
45
|
registerReleaseCommand(pi);
|
|
45
46
|
registerUpdateCommand(pi);
|
|
47
|
+
registerFixPrCommand(pi);
|
|
46
48
|
|
|
47
49
|
// Intercept TUI-only commands at the input level — this runs BEFORE
|
|
48
50
|
// message submission, so no chat message appears and no "Working..." indicator
|
package/src/qa/config.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { E2eQaConfig } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const CONFIG_FILENAME = "e2e-qa.json";
|
|
6
|
+
|
|
7
|
+
function getConfigPath(cwd: string): string {
|
|
8
|
+
return path.join(cwd, ".omp", "supipowers", CONFIG_FILENAME);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_E2E_QA_CONFIG: E2eQaConfig = {
|
|
12
|
+
app: {
|
|
13
|
+
type: "generic",
|
|
14
|
+
devCommand: "npm run dev",
|
|
15
|
+
port: 3000,
|
|
16
|
+
baseUrl: "http://localhost:3000",
|
|
17
|
+
},
|
|
18
|
+
playwright: {
|
|
19
|
+
browser: "chromium",
|
|
20
|
+
headless: true,
|
|
21
|
+
timeout: 30000,
|
|
22
|
+
},
|
|
23
|
+
execution: {
|
|
24
|
+
maxRetries: 2,
|
|
25
|
+
maxFlows: 20,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function loadE2eQaConfig(cwd: string): E2eQaConfig | null {
|
|
30
|
+
const configPath = getConfigPath(cwd);
|
|
31
|
+
if (!fs.existsSync(configPath)) return null;
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(fs.readFileSync(configPath, "utf-8")) as E2eQaConfig;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function saveE2eQaConfig(cwd: string, config: E2eQaConfig): void {
|
|
40
|
+
const configPath = getConfigPath(cwd);
|
|
41
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
42
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
43
|
+
}
|
package/src/qa/matrix.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { E2eMatrix, E2eFlowRecord, E2eTestResult, E2eRegression } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const MATRIX_FILENAME = "e2e-matrix.json";
|
|
6
|
+
|
|
7
|
+
function getMatrixPath(cwd: string): string {
|
|
8
|
+
return path.join(cwd, ".omp", "supipowers", MATRIX_FILENAME);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createEmptyMatrix(appType: string): E2eMatrix {
|
|
12
|
+
return {
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
updatedAt: new Date().toISOString(),
|
|
15
|
+
appType,
|
|
16
|
+
flows: [],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function loadE2eMatrix(cwd: string): E2eMatrix | null {
|
|
21
|
+
const matrixPath = getMatrixPath(cwd);
|
|
22
|
+
if (!fs.existsSync(matrixPath)) return null;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(fs.readFileSync(matrixPath, "utf-8")) as E2eMatrix;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function saveE2eMatrix(cwd: string, matrix: E2eMatrix): void {
|
|
31
|
+
const matrixPath = getMatrixPath(cwd);
|
|
32
|
+
fs.mkdirSync(path.dirname(matrixPath), { recursive: true });
|
|
33
|
+
fs.writeFileSync(matrixPath, JSON.stringify(matrix, null, 2));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function detectRegressions(
|
|
37
|
+
previousFlows: E2eFlowRecord[],
|
|
38
|
+
results: E2eTestResult[],
|
|
39
|
+
): E2eRegression[] {
|
|
40
|
+
const regressions: E2eRegression[] = [];
|
|
41
|
+
|
|
42
|
+
for (const result of results) {
|
|
43
|
+
if (result.status !== "fail") continue;
|
|
44
|
+
|
|
45
|
+
const previousFlow = previousFlows.find((f) => f.id === result.flowId);
|
|
46
|
+
if (!previousFlow || previousFlow.lastStatus !== "pass") continue;
|
|
47
|
+
|
|
48
|
+
regressions.push({
|
|
49
|
+
flowId: result.flowId,
|
|
50
|
+
flowName: previousFlow.name,
|
|
51
|
+
previousStatus: "pass",
|
|
52
|
+
currentStatus: "fail",
|
|
53
|
+
error: result.error ?? "Unknown error",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return regressions;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function updateMatrixFromResults(
|
|
61
|
+
matrix: E2eMatrix,
|
|
62
|
+
results: E2eTestResult[],
|
|
63
|
+
): E2eMatrix {
|
|
64
|
+
const now = new Date().toISOString();
|
|
65
|
+
const resultMap = new Map(results.map((r) => [r.flowId, r]));
|
|
66
|
+
|
|
67
|
+
const updatedFlows = matrix.flows.map((flow) => {
|
|
68
|
+
const result = resultMap.get(flow.id);
|
|
69
|
+
if (!result) return flow;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
...flow,
|
|
73
|
+
lastStatus: result.status === "skip" ? flow.lastStatus : (result.status as "pass" | "fail"),
|
|
74
|
+
lastTestedAt: now,
|
|
75
|
+
lastError: result.status === "fail" ? result.error : undefined,
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
...matrix,
|
|
81
|
+
updatedAt: now,
|
|
82
|
+
flows: updatedFlows,
|
|
83
|
+
};
|
|
84
|
+
}
|