supipowers 2.2.0 → 2.2.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.
- package/README.md +71 -12
- package/package.json +4 -8
- package/skills/ui-design/SKILL.md +2 -2
- package/src/ai/final-message.ts +15 -1
- package/src/ai/schema-text.ts +60 -40
- package/src/ai/schema-validation.ts +88 -0
- package/src/ai/structured-output.ts +19 -19
- package/src/commands/fix-pr.ts +166 -26
- package/src/config/schema.ts +102 -139
- package/src/docs/contracts.ts +13 -23
- package/src/fix-pr/assessment.ts +63 -24
- package/src/fix-pr/contracts.ts +15 -23
- package/src/fix-pr/fetch-comments.ts +119 -0
- package/src/fix-pr/prompt-builder.ts +19 -8
- package/src/git/commit-contract.ts +13 -19
- package/src/git/commit.ts +168 -6
- package/src/lsp/capabilities.ts +9 -12
- package/src/lsp/contracts.ts +15 -23
- package/src/planning/planning-ask-tool.ts +13 -2
- package/src/planning/spec.ts +21 -27
- package/src/planning/system-prompt.ts +1 -1
- package/src/planning/validate.ts +4 -7
- package/src/platform/progress.ts +11 -0
- package/src/quality/contracts.ts +15 -23
- package/src/quality/schemas.ts +40 -67
- package/src/release/contracts.ts +19 -28
- package/src/review/types.ts +142 -186
- package/src/types.ts +5 -2
- package/src/ui-design/session.ts +13 -2
- package/src/ui-design/system-prompt.ts +2 -2
- package/src/ultraplan/contracts.ts +458 -524
package/src/fix-pr/assessment.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface RunFixPrAssessmentInput {
|
|
|
29
29
|
model?: string;
|
|
30
30
|
thinkingLevel?: string | null;
|
|
31
31
|
maxAttempts?: number;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
maxCommentsPerBatch?: number;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
interface BuildAssessmentPromptArgs {
|
|
@@ -74,6 +76,19 @@ function buildAssessmentPrompt(args: BuildAssessmentPromptArgs): string {
|
|
|
74
76
|
].join("\n");
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
function chunkComments(comments: readonly PrComment[], maxCommentsPerBatch: number): PrComment[][] {
|
|
80
|
+
if (maxCommentsPerBatch <= 0 || comments.length <= maxCommentsPerBatch) {
|
|
81
|
+
return [[...comments]];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const chunks: PrComment[][] = [];
|
|
85
|
+
for (let index = 0; index < comments.length; index += maxCommentsPerBatch) {
|
|
86
|
+
chunks.push(comments.slice(index, index + maxCommentsPerBatch));
|
|
87
|
+
}
|
|
88
|
+
return chunks;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
77
92
|
/**
|
|
78
93
|
* Run a schema-backed assessment over a cluster of PR comments.
|
|
79
94
|
*
|
|
@@ -93,30 +108,54 @@ export async function runFixPrAssessment(
|
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
const schemaText = renderSchemaText(FixPrAssessmentBatchSchema);
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
:
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
const maxCommentsPerBatch = input.maxCommentsPerBatch ?? input.comments.length;
|
|
112
|
+
const commentChunks = chunkComments(input.comments, maxCommentsPerBatch);
|
|
113
|
+
const assessments: FixPrAssessmentBatch["assessments"] = [];
|
|
114
|
+
const rawOutputs: string[] = [];
|
|
115
|
+
let attempts = 0;
|
|
116
|
+
|
|
117
|
+
for (const comments of commentChunks) {
|
|
118
|
+
const prompt = buildAssessmentPrompt({
|
|
119
|
+
schemaText,
|
|
120
|
+
comments,
|
|
121
|
+
repo: input.repo,
|
|
122
|
+
prNumber: input.prNumber,
|
|
123
|
+
selectedTargetLabel: input.selectedTargetLabel,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const result = await runWithOutputValidation<FixPrAssessmentBatch>(
|
|
127
|
+
input.createAgentSession as any,
|
|
128
|
+
{
|
|
129
|
+
cwd: input.cwd,
|
|
130
|
+
prompt,
|
|
131
|
+
schema: schemaText,
|
|
132
|
+
parse: (raw) =>
|
|
133
|
+
parseStructuredOutput<FixPrAssessmentBatch>(raw, FixPrAssessmentBatchSchema),
|
|
134
|
+
model: input.model,
|
|
135
|
+
thinkingLevel: input.thinkingLevel ?? null,
|
|
136
|
+
maxAttempts: input.maxAttempts,
|
|
137
|
+
timeoutMs: input.timeoutMs,
|
|
138
|
+
reliability: input.paths
|
|
139
|
+
? { paths: input.paths, cwd: input.cwd, command: "fix-pr", operation: "assessment" }
|
|
140
|
+
: undefined,
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
attempts += result.attempts;
|
|
145
|
+
if (result.status === "blocked") {
|
|
146
|
+
return { ...result, attempts };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
rawOutputs.push(result.rawOutput);
|
|
150
|
+
assessments.push(...result.output.assessments);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
status: "ok",
|
|
155
|
+
output: { assessments },
|
|
156
|
+
rawOutput: rawOutputs.join("\n"),
|
|
157
|
+
attempts,
|
|
158
|
+
};
|
|
120
159
|
}
|
|
121
160
|
|
|
122
161
|
/**
|
package/src/fix-pr/contracts.ts
CHANGED
|
@@ -5,35 +5,27 @@
|
|
|
5
5
|
// against FixPrAssessmentBatchSchema; downstream work batches are derived
|
|
6
6
|
// from this validated artifact, not from ad-hoc orchestration prose.
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { z } from "zod/v4"
|
|
9
9
|
|
|
10
10
|
export const FIX_PR_ASSESSMENT_VERDICTS = ["apply", "reject", "investigate"] as const;
|
|
11
11
|
export type FixPrAssessmentVerdict = (typeof FIX_PR_ASSESSMENT_VERDICTS)[number];
|
|
12
12
|
|
|
13
|
-
export const FixPrCommentAssessmentSchema =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
rippleEffects: Type.Array(Type.String({ minLength: 1 })),
|
|
22
|
-
verificationPlan: Type.String({ minLength: 1 }),
|
|
23
|
-
},
|
|
24
|
-
{ additionalProperties: false },
|
|
25
|
-
);
|
|
13
|
+
export const FixPrCommentAssessmentSchema = z.object({
|
|
14
|
+
commentId: z.number().int(),
|
|
15
|
+
verdict: z.enum(FIX_PR_ASSESSMENT_VERDICTS),
|
|
16
|
+
rationale: z.string().min(1),
|
|
17
|
+
affectedFiles: z.array(z.string().min(1)),
|
|
18
|
+
rippleEffects: z.array(z.string().min(1)),
|
|
19
|
+
verificationPlan: z.string().min(1),
|
|
20
|
+
}).strict();
|
|
26
21
|
|
|
27
|
-
export const FixPrAssessmentBatchSchema =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
},
|
|
32
|
-
{ additionalProperties: false },
|
|
33
|
-
);
|
|
22
|
+
export const FixPrAssessmentBatchSchema = z.object({
|
|
23
|
+
assessments: z.array(FixPrCommentAssessmentSchema),
|
|
24
|
+
summary: z.string().optional(),
|
|
25
|
+
}).strict();
|
|
34
26
|
|
|
35
|
-
export type FixPrCommentAssessment =
|
|
36
|
-
export type FixPrAssessmentBatch =
|
|
27
|
+
export type FixPrCommentAssessment = z.infer<typeof FixPrCommentAssessmentSchema>;
|
|
28
|
+
export type FixPrAssessmentBatch = z.infer<typeof FixPrAssessmentBatchSchema>;
|
|
37
29
|
|
|
38
30
|
/**
|
|
39
31
|
* A deterministic execution unit derived from a validated FixPrAssessmentBatch.
|
|
@@ -11,6 +11,30 @@ const INLINE_COMMENTS_JQ =
|
|
|
11
11
|
const REVIEW_COMMENTS_JQ =
|
|
12
12
|
'.[] | select(.body != null and .body != "") | {id, path: null, line: null, body, user: .user.login, userType: .user.type, createdAt: .submitted_at, updatedAt: .submitted_at, inReplyToId: null, diffHunk: null, state}';
|
|
13
13
|
|
|
14
|
+
|
|
15
|
+
const RESOLVED_REVIEW_THREAD_COMMENT_IDS_QUERY = `
|
|
16
|
+
query($owner: String!, $name: String!, $number: Int!, $endCursor: String) {
|
|
17
|
+
repository(owner: $owner, name: $name) {
|
|
18
|
+
pullRequest(number: $number) {
|
|
19
|
+
reviewThreads(first: 100, after: $endCursor) {
|
|
20
|
+
nodes {
|
|
21
|
+
isResolved
|
|
22
|
+
comments(first: 100) {
|
|
23
|
+
nodes {
|
|
24
|
+
databaseId
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
pageInfo {
|
|
29
|
+
hasNextPage
|
|
30
|
+
endCursor
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
|
|
14
38
|
export interface ClusteredPrComments<TTarget extends WorkspaceTarget = WorkspaceTarget> {
|
|
15
39
|
allComments: PrComment[];
|
|
16
40
|
commentsByTargetId: Map<string, PrComment[]>;
|
|
@@ -27,6 +51,30 @@ function appendComment(commentsByTargetId: Map<string, PrComment[]>, targetId: s
|
|
|
27
51
|
commentsByTargetId.set(targetId, [comment]);
|
|
28
52
|
}
|
|
29
53
|
|
|
54
|
+
|
|
55
|
+
function parseRepoOwnerAndName(repo: string): { owner: string; name: string } | null {
|
|
56
|
+
const separator = repo.indexOf("/");
|
|
57
|
+
if (separator <= 0 || separator === repo.length - 1) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
owner: repo.slice(0, separator),
|
|
63
|
+
name: repo.slice(separator + 1),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseResolvedCommentIds(output: string): Set<number> {
|
|
68
|
+
const ids = new Set<number>();
|
|
69
|
+
for (const line of output.split(/\r?\n/)) {
|
|
70
|
+
const id = Number.parseInt(line.trim(), 10);
|
|
71
|
+
if (Number.isInteger(id)) {
|
|
72
|
+
ids.add(id);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return ids;
|
|
76
|
+
}
|
|
77
|
+
|
|
30
78
|
export function parsePrCommentsJsonl(content: string): PrComment[] {
|
|
31
79
|
return content
|
|
32
80
|
.split(/\r?\n/)
|
|
@@ -49,6 +97,68 @@ export function stringifyPrCommentsJsonl(comments: readonly PrComment[]): string
|
|
|
49
97
|
return `${comments.map((comment) => JSON.stringify(comment)).join("\n")}\n`;
|
|
50
98
|
}
|
|
51
99
|
|
|
100
|
+
async function fetchResolvedReviewThreadCommentIds(
|
|
101
|
+
platform: Platform,
|
|
102
|
+
repo: string,
|
|
103
|
+
prNumber: number,
|
|
104
|
+
cwd: string,
|
|
105
|
+
): Promise<Set<number> | string> {
|
|
106
|
+
const repoParts = parseRepoOwnerAndName(repo);
|
|
107
|
+
if (!repoParts) {
|
|
108
|
+
return `Invalid repository name: ${repo}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const result = await platform.exec(
|
|
112
|
+
"gh",
|
|
113
|
+
[
|
|
114
|
+
"api",
|
|
115
|
+
"graphql",
|
|
116
|
+
"--paginate",
|
|
117
|
+
"-f",
|
|
118
|
+
`query=${RESOLVED_REVIEW_THREAD_COMMENT_IDS_QUERY}`,
|
|
119
|
+
"-F",
|
|
120
|
+
`owner=${repoParts.owner}`,
|
|
121
|
+
"-F",
|
|
122
|
+
`name=${repoParts.name}`,
|
|
123
|
+
"-F",
|
|
124
|
+
`number=${prNumber}`,
|
|
125
|
+
"--jq",
|
|
126
|
+
".data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == true) | .comments.nodes[].databaseId",
|
|
127
|
+
],
|
|
128
|
+
{ cwd },
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (result.code !== 0) {
|
|
132
|
+
return result.stderr || "gh api graphql failed while fetching resolved review threads";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return parseResolvedCommentIds(result.stdout);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function filterResolvedComments(content: string, resolvedCommentIds: ReadonlySet<number>): string {
|
|
139
|
+
if (!content.trim() || resolvedCommentIds.size === 0) {
|
|
140
|
+
return content;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const unresolvedLines = content
|
|
144
|
+
.split(/\r?\n/)
|
|
145
|
+
.filter((line) => {
|
|
146
|
+
const trimmed = line.trim();
|
|
147
|
+
if (!trimmed) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const comment = JSON.parse(trimmed) as Pick<PrComment, "id">;
|
|
152
|
+
return !resolvedCommentIds.has(comment.id);
|
|
153
|
+
} catch {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return unresolvedLines.length > 0 ? `${unresolvedLines.join("\n")}\n` : "";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
52
162
|
export function clusterPrCommentsByTarget<TTarget extends WorkspaceTarget>(
|
|
53
163
|
targets: readonly TTarget[],
|
|
54
164
|
comments: readonly PrComment[],
|
|
@@ -143,4 +253,13 @@ export async function fetchPrComments(
|
|
|
143
253
|
if (inlineResult.code !== 0 && reviewResult.code !== 0) {
|
|
144
254
|
return inlineResult.stderr || reviewResult.stderr || "gh api calls failed";
|
|
145
255
|
}
|
|
256
|
+
|
|
257
|
+
const resolvedCommentIds = await fetchResolvedReviewThreadCommentIds(platform, repo, prNumber, cwd);
|
|
258
|
+
if (typeof resolvedCommentIds === "string") {
|
|
259
|
+
return resolvedCommentIds;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (resolvedCommentIds.size > 0) {
|
|
263
|
+
fs.writeFileSync(outputPath, filterResolvedComments(fs.readFileSync(outputPath, "utf-8"), resolvedCommentIds));
|
|
264
|
+
}
|
|
146
265
|
}
|
|
@@ -177,26 +177,37 @@ export function buildFixPrOrchestratorPrompt(options: FixPrPromptOptions): strin
|
|
|
177
177
|
sections.push(buildReplyInstructions(config), "");
|
|
178
178
|
|
|
179
179
|
sections.push(
|
|
180
|
-
"## Step 6: Push and Check for New Comments",
|
|
180
|
+
"## Step 6: Push, Monitor CI, and Check for New Comments",
|
|
181
181
|
"",
|
|
182
|
-
`1. Stage and commit: \`git add -A && git commit -m
|
|
182
|
+
`1. Stage and commit: \`git add -A && git commit -m "fix: address PR review comments (iteration ${iteration})"\``,
|
|
183
183
|
"2. Push: `git push`",
|
|
184
184
|
);
|
|
185
185
|
|
|
186
186
|
if (reviewer.type !== "none" && reviewer.triggerMethod) {
|
|
187
187
|
sections.push(
|
|
188
|
-
`3. Trigger re-review: \`bun
|
|
188
|
+
`3. Trigger re-review: \`bun "${scriptsDir}/trigger-review.ts" "${repo}" ${prNumber} "${reviewer.type}" "${reviewer.triggerMethod}"\``,
|
|
189
|
+
`4. While the reviewer runs, start the green pipeline: invoke OMP \`/green\` if available; otherwise run \`gh pr checks ${prNumber} --repo ${repo} --watch\`.`,
|
|
190
|
+
" - If CI turns red, stop waiting for review comments and focus on CI.",
|
|
191
|
+
" - Diagnose the failed check from its logs, fix the root cause, push again, then restart green monitoring.",
|
|
192
|
+
"5. Run the wait-and-check runner:",
|
|
193
|
+
);
|
|
194
|
+
} else {
|
|
195
|
+
sections.push(
|
|
196
|
+
`3. Start the green pipeline: invoke OMP \`/green\` if available; otherwise run \`gh pr checks ${prNumber} --repo ${repo} --watch\`.`,
|
|
197
|
+
" - If CI turns red, focus on CI before considering the PR complete.",
|
|
198
|
+
" - Diagnose the failed check from its logs, fix the root cause, push again, then restart green monitoring.",
|
|
199
|
+
"4. Run the wait-and-check runner:",
|
|
189
200
|
);
|
|
190
201
|
}
|
|
191
202
|
|
|
192
203
|
sections.push(
|
|
193
|
-
`${reviewer.type !== "none" ? "4" : "3"}. Run the wait-and-check runner:`,
|
|
194
204
|
"```text",
|
|
195
|
-
`bun
|
|
205
|
+
`bun "${scriptsDir}/wait-and-check.ts" "${sessionDir}" ${delay} ${iteration + 1} "${repo}" ${prNumber}`,
|
|
196
206
|
"```",
|
|
197
|
-
`${reviewer.type !== "none" ? "
|
|
198
|
-
` - If \`hasNewComments: true\` and iteration < ${maxIter}: process the new comments (go back to Step 1)`,
|
|
199
|
-
` - If \`hasNewComments: false\` or iteration >= ${maxIter}:
|
|
207
|
+
`${reviewer.type !== "none" ? "6" : "5"}. Read the last line of output:`,
|
|
208
|
+
` - If \`hasNewComments: true\` and iteration < ${maxIter}: process the new comments (go back to Step 1), then repeat push, green monitoring, and final validation`,
|
|
209
|
+
` - If \`hasNewComments: false\` or iteration >= ${maxIter}: continue only after CI is green`,
|
|
210
|
+
`${reviewer.type !== "none" ? "7" : "6"}. Full validation is mandatory at the end: run \`bun ci\` locally after comments are handled and CI is green. Do not report done until both remote CI and local full validation are green.`,
|
|
200
211
|
"",
|
|
201
212
|
);
|
|
202
213
|
|
|
@@ -6,30 +6,24 @@
|
|
|
6
6
|
// (every staged file appears in exactly one commit) is a runtime rule that
|
|
7
7
|
// can't live in the schema — see validateCommitPlanCoverage.
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { z } from "zod/v4"
|
|
10
10
|
import { VALID_COMMIT_TYPES } from "../release/commit-types.js";
|
|
11
11
|
import type { ValidationError } from "../types.js";
|
|
12
12
|
|
|
13
|
-
export const CommitGroupSchema =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
},
|
|
21
|
-
{ additionalProperties: false },
|
|
22
|
-
);
|
|
13
|
+
export const CommitGroupSchema = z.object({
|
|
14
|
+
type: z.enum(VALID_COMMIT_TYPES),
|
|
15
|
+
scope: z.string().nullable(),
|
|
16
|
+
summary: z.string().min(1),
|
|
17
|
+
details: z.array(z.string()),
|
|
18
|
+
files: z.array(z.string().min(1)).min(1),
|
|
19
|
+
}).strict();
|
|
23
20
|
|
|
24
|
-
export const CommitPlanSchema =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
},
|
|
28
|
-
{ additionalProperties: false },
|
|
29
|
-
);
|
|
21
|
+
export const CommitPlanSchema = z.object({
|
|
22
|
+
commits: z.array(CommitGroupSchema).min(1),
|
|
23
|
+
}).strict();
|
|
30
24
|
|
|
31
|
-
export type CommitGroup =
|
|
32
|
-
export type CommitPlan =
|
|
25
|
+
export type CommitGroup = z.infer<typeof CommitGroupSchema>;
|
|
26
|
+
export type CommitPlan = z.infer<typeof CommitPlanSchema>;
|
|
33
27
|
|
|
34
28
|
/**
|
|
35
29
|
* Verify every staged file appears in exactly one commit and that no commit
|
package/src/git/commit.ts
CHANGED
|
@@ -103,6 +103,8 @@ const COMMIT_STEPS = [
|
|
|
103
103
|
{ key: "ai-analysis", label: "AI analysis" },
|
|
104
104
|
{ key: "review-plan", label: "Review plan" },
|
|
105
105
|
{ key: "execute-commits", label: "Execute commits" },
|
|
106
|
+
{ key: "push-commits", label: "Push commits" },
|
|
107
|
+
{ key: "open-pr", label: "Open pull request" },
|
|
106
108
|
] as const;
|
|
107
109
|
|
|
108
110
|
function createProgress(ctx: any) {
|
|
@@ -127,6 +129,9 @@ function createProgress(ctx: any) {
|
|
|
127
129
|
skip(stepIndex: number, detail?: string) {
|
|
128
130
|
progress.skip(COMMIT_STEPS[stepIndex]!.key, detail);
|
|
129
131
|
},
|
|
132
|
+
fail(stepIndex: number, detail?: string) {
|
|
133
|
+
progress.fail(COMMIT_STEPS[stepIndex]!.key, detail);
|
|
134
|
+
},
|
|
130
135
|
dispose() {
|
|
131
136
|
progress.dispose();
|
|
132
137
|
},
|
|
@@ -340,14 +345,11 @@ export async function analyzeAndCommit(
|
|
|
340
345
|
}
|
|
341
346
|
|
|
342
347
|
if (!plan) {
|
|
343
|
-
// Skip remaining tracked steps for the manual path
|
|
344
348
|
progress.skip(5, "manual");
|
|
345
|
-
progress.skip(6, "manual");
|
|
346
|
-
progress.dispose();
|
|
347
349
|
const reason = !platform.capabilities.agentSessions
|
|
348
350
|
? "no agent sessions"
|
|
349
351
|
: agentReason;
|
|
350
|
-
return manualFallback(platform, ctx, cwd, fileList, platform.paths, agentAttempts, reason);
|
|
352
|
+
return manualFallback(platform, ctx, cwd, fileList, platform.paths, agentAttempts, progress, reason);
|
|
351
353
|
}
|
|
352
354
|
|
|
353
355
|
// 6. Present plan for approval
|
|
@@ -444,6 +446,7 @@ async function manualFallback(
|
|
|
444
446
|
fileList: string[],
|
|
445
447
|
paths: PlatformPaths,
|
|
446
448
|
attempts: number,
|
|
449
|
+
progress: ReturnType<typeof createProgress>,
|
|
447
450
|
reason?: string,
|
|
448
451
|
): Promise<CommitResult | null> {
|
|
449
452
|
const exec = platform.exec.bind(platform);
|
|
@@ -467,27 +470,186 @@ async function manualFallback(
|
|
|
467
470
|
? `${reason} \u2014 enter a commit message manually`
|
|
468
471
|
: "Enter a commit message manually",
|
|
469
472
|
);
|
|
473
|
+
progress.activate(6, "manual");
|
|
470
474
|
|
|
471
475
|
const message = await ctx.ui.input("Commit message (empty to abort)", {
|
|
472
476
|
helpText: `${fileList.length} file(s) staged`,
|
|
473
477
|
});
|
|
474
|
-
|
|
475
478
|
if (!message?.trim()) {
|
|
476
479
|
notifyInfo(ctx, "Commit cancelled", "No message provided");
|
|
480
|
+
progress.skip(6, "aborted");
|
|
477
481
|
return null;
|
|
478
482
|
}
|
|
479
483
|
|
|
480
484
|
const commitResult = await commitStaged(exec, cwd, message);
|
|
481
485
|
if (!commitResult.success) {
|
|
482
486
|
notifyError(ctx, "Commit failed", commitResult.error);
|
|
487
|
+
progress.fail(6, "failed");
|
|
483
488
|
return null;
|
|
484
489
|
}
|
|
485
490
|
|
|
486
491
|
notifySuccess(ctx, "Committed", message.split("\n")[0]);
|
|
492
|
+
progress.complete(6, "1 done");
|
|
493
|
+
await offerPostCommitActions(platform, ctx, cwd, progress);
|
|
487
494
|
return { committed: 1, messages: [message] };
|
|
488
495
|
}
|
|
489
496
|
|
|
490
497
|
|
|
498
|
+
const PUSH_NO_OPTION = "No — keep commits local";
|
|
499
|
+
const PR_NO_OPTION = "No — leave branch without a PR";
|
|
500
|
+
const PR_YES_OPTION = "Yes — open a Pull Request";
|
|
501
|
+
|
|
502
|
+
function pushYesOption(branch: string): string {
|
|
503
|
+
return `Yes — push to origin/${branch}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function isDefaultBranchName(branch: string): boolean {
|
|
507
|
+
return branch === "main" || branch === "master";
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function formatCommandFailure(result: { stdout: string; stderr: string; code: number }): string {
|
|
511
|
+
return result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function readCurrentBranch(
|
|
515
|
+
exec: ExecFn,
|
|
516
|
+
ctx: any,
|
|
517
|
+
cwd: string,
|
|
518
|
+
): Promise<string | null> {
|
|
519
|
+
const result = await exec("git", ["branch", "--show-current"], { cwd });
|
|
520
|
+
if (result.code !== 0) {
|
|
521
|
+
notifyError(ctx, "Could not determine current branch", formatCommandFailure(result));
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return result.stdout.trim() || null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function pushCurrentBranch(
|
|
529
|
+
exec: ExecFn,
|
|
530
|
+
ctx: any,
|
|
531
|
+
cwd: string,
|
|
532
|
+
branch: string,
|
|
533
|
+
): Promise<boolean> {
|
|
534
|
+
const result = await exec("git", ["push", "-u", "origin", branch], { cwd });
|
|
535
|
+
if (result.code !== 0) {
|
|
536
|
+
notifyError(ctx, "Push failed", formatCommandFailure(result));
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
notifySuccess(ctx, "Pushed", `origin/${branch}`);
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function createPullRequest(
|
|
545
|
+
exec: ExecFn,
|
|
546
|
+
ctx: any,
|
|
547
|
+
cwd: string,
|
|
548
|
+
branch: string,
|
|
549
|
+
): Promise<boolean> {
|
|
550
|
+
const result = await exec("gh", ["pr", "create", "--fill", "--head", branch], { cwd });
|
|
551
|
+
if (result.code !== 0) {
|
|
552
|
+
notifyError(ctx, "Pull request failed", formatCommandFailure(result));
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const detail = result.stdout.trim() || result.stderr.trim() || `Branch: ${branch}`;
|
|
557
|
+
notifySuccess(ctx, "Pull request opened", detail);
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function offerPostCommitActions(
|
|
562
|
+
platform: Platform,
|
|
563
|
+
ctx: any,
|
|
564
|
+
cwd: string,
|
|
565
|
+
progress: ReturnType<typeof createProgress>,
|
|
566
|
+
): Promise<void> {
|
|
567
|
+
try {
|
|
568
|
+
const exec = platform.exec.bind(platform);
|
|
569
|
+
progress.activate(7, "detect branch");
|
|
570
|
+
const branch = await readCurrentBranch(exec, ctx, cwd);
|
|
571
|
+
if (!branch) {
|
|
572
|
+
progress.skip(7, "no branch");
|
|
573
|
+
progress.skip(8, "no branch");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const yesPush = pushYesOption(branch);
|
|
578
|
+
progress.activate(7, "prompt");
|
|
579
|
+
const pushSelection = await ctx.ui.select("Push commits?", [
|
|
580
|
+
PUSH_NO_OPTION,
|
|
581
|
+
yesPush,
|
|
582
|
+
], {
|
|
583
|
+
helpText: `Current branch: ${branch}`,
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
if (!pushSelection) {
|
|
587
|
+
progress.skip(7, "cancelled");
|
|
588
|
+
progress.skip(8, "cancelled");
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
let pushed = false;
|
|
593
|
+
if (pushSelection === yesPush) {
|
|
594
|
+
progress.activate(7, `origin/${branch}`);
|
|
595
|
+
pushed = await pushCurrentBranch(exec, ctx, cwd, branch);
|
|
596
|
+
if (!pushed) {
|
|
597
|
+
progress.fail(7, "failed");
|
|
598
|
+
progress.skip(8, "push failed");
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
progress.complete(7, `origin/${branch}`);
|
|
602
|
+
} else {
|
|
603
|
+
progress.skip(7, "kept local");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (isDefaultBranchName(branch)) {
|
|
607
|
+
progress.skip(8, "default branch");
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
progress.activate(8, "prompt");
|
|
612
|
+
const prSelection = await ctx.ui.select("Open a Pull Request?", [
|
|
613
|
+
PR_NO_OPTION,
|
|
614
|
+
PR_YES_OPTION,
|
|
615
|
+
], {
|
|
616
|
+
helpText: pushed
|
|
617
|
+
? `Branch: ${branch}`
|
|
618
|
+
: `Opening a PR will first push origin/${branch}.`,
|
|
619
|
+
});
|
|
620
|
+
if (!prSelection) {
|
|
621
|
+
progress.skip(8, "cancelled");
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (prSelection !== PR_YES_OPTION) {
|
|
625
|
+
progress.skip(8, "not requested");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (!pushed) {
|
|
630
|
+
progress.activate(7, `origin/${branch}`);
|
|
631
|
+
pushed = await pushCurrentBranch(exec, ctx, cwd, branch);
|
|
632
|
+
if (!pushed) {
|
|
633
|
+
progress.fail(7, "failed");
|
|
634
|
+
progress.skip(8, "push failed");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
progress.complete(7, `origin/${branch}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
progress.activate(8, "gh pr create");
|
|
641
|
+
if (!await createPullRequest(exec, ctx, cwd, branch)) {
|
|
642
|
+
progress.fail(8, "failed");
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
progress.complete(8, "created");
|
|
646
|
+
} catch (err) {
|
|
647
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
648
|
+
progress.fail(8, "error");
|
|
649
|
+
notifyError(ctx, "Post-commit action failed", message);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
491
653
|
|
|
492
654
|
// ── Commit execution ───────────────────────────────────────
|
|
493
655
|
|
|
@@ -548,12 +710,12 @@ async function executeCommitPlan(
|
|
|
548
710
|
await exec("git", ["read-tree", savedTree], { cwd });
|
|
549
711
|
|
|
550
712
|
progress.complete(6, `${committedMessages.length} done`);
|
|
551
|
-
progress.dispose();
|
|
552
713
|
notifySuccess(
|
|
553
714
|
ctx,
|
|
554
715
|
`${committedMessages.length} commit(s) created`,
|
|
555
716
|
committedMessages.map((m) => m.split("\n")[0]).join(" | "),
|
|
556
717
|
);
|
|
718
|
+
await offerPostCommitActions(platform, ctx, cwd, progress);
|
|
557
719
|
|
|
558
720
|
return { committed: committedMessages.length, messages: committedMessages };
|
|
559
721
|
}
|
package/src/lsp/capabilities.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// treated as fail-closed: NO_LSP_SUPPORT (gate skips rather than pretending
|
|
12
12
|
// it ran).
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { z } from "zod/v4";
|
|
15
15
|
import {
|
|
16
16
|
parseStructuredOutput,
|
|
17
17
|
runWithOutputValidation,
|
|
@@ -20,18 +20,15 @@ import {
|
|
|
20
20
|
import { renderSchemaText } from "../ai/schema-text.js";
|
|
21
21
|
import type { GateExecutionContext } from "../types.js";
|
|
22
22
|
|
|
23
|
-
export const LspCapabilitiesSchema =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
{ additionalProperties: false },
|
|
32
|
-
);
|
|
23
|
+
export const LspCapabilitiesSchema = z.object({
|
|
24
|
+
diagnostics: z.boolean(),
|
|
25
|
+
references: z.boolean(),
|
|
26
|
+
definition: z.boolean(),
|
|
27
|
+
hover: z.boolean(),
|
|
28
|
+
rename: z.boolean(),
|
|
29
|
+
}).strict();
|
|
33
30
|
|
|
34
|
-
export type LspCapabilities =
|
|
31
|
+
export type LspCapabilities = z.infer<typeof LspCapabilitiesSchema>;
|
|
35
32
|
|
|
36
33
|
const SCHEMA_TEXT = renderSchemaText(LspCapabilitiesSchema);
|
|
37
34
|
|