supipowers 2.1.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.
Files changed (44) hide show
  1. package/README.md +71 -12
  2. package/package.json +4 -8
  3. package/skills/ui-design/SKILL.md +2 -2
  4. package/src/ai/final-message.ts +15 -1
  5. package/src/ai/schema-text.ts +60 -40
  6. package/src/ai/schema-validation.ts +88 -0
  7. package/src/ai/structured-output.ts +19 -19
  8. package/src/bootstrap.ts +3 -0
  9. package/src/commands/fix-pr.ts +166 -26
  10. package/src/commands/optimize-context.ts +153 -16
  11. package/src/commands/runbook.ts +511 -0
  12. package/src/config/schema.ts +102 -139
  13. package/src/context/rule-renderer.ts +274 -2
  14. package/src/context/runbook-extension-template.ts +193 -0
  15. package/src/context/startup-check.ts +197 -2
  16. package/src/context/startup-optimizer.ts +133 -10
  17. package/src/docs/contracts.ts +13 -23
  18. package/src/fix-pr/assessment.ts +63 -24
  19. package/src/fix-pr/contracts.ts +15 -23
  20. package/src/fix-pr/fetch-comments.ts +119 -0
  21. package/src/fix-pr/prompt-builder.ts +19 -8
  22. package/src/git/commit-contract.ts +13 -19
  23. package/src/git/commit.ts +168 -6
  24. package/src/harness/command.ts +98 -6
  25. package/src/harness/git-verification.ts +515 -0
  26. package/src/harness/git-verify-qa.ts +406 -0
  27. package/src/harness/pipeline.ts +17 -8
  28. package/src/harness/stages/implement-apply.ts +61 -4
  29. package/src/harness/stages/validate.ts +108 -0
  30. package/src/lsp/capabilities.ts +9 -12
  31. package/src/lsp/contracts.ts +15 -23
  32. package/src/planning/planning-ask-tool.ts +13 -2
  33. package/src/planning/spec.ts +21 -27
  34. package/src/planning/system-prompt.ts +1 -1
  35. package/src/planning/validate.ts +4 -7
  36. package/src/platform/progress.ts +11 -0
  37. package/src/quality/contracts.ts +15 -23
  38. package/src/quality/schemas.ts +40 -67
  39. package/src/release/contracts.ts +19 -28
  40. package/src/review/types.ts +142 -186
  41. package/src/types.ts +45 -2
  42. package/src/ui-design/session.ts +13 -2
  43. package/src/ui-design/system-prompt.ts +2 -2
  44. package/src/ultraplan/contracts.ts +458 -524
@@ -5,7 +5,7 @@
5
5
  // retry loop (runWithOutputValidation) will hand validation errors back to
6
6
  // the model rather than letting a silent regex heuristic invent findings.
7
7
 
8
- import { Type, type Static } from "@sinclair/typebox";
8
+ import { z } from "zod/v4"
9
9
 
10
10
  export const DOC_DRIFT_SEVERITIES = ["info", "warning", "error"] as const;
11
11
  export const DOC_DRIFT_STATUSES = ["ok", "drifted"] as const;
@@ -13,27 +13,17 @@ export const DOC_DRIFT_STATUSES = ["ok", "drifted"] as const;
13
13
  export type DocDriftSeverity = (typeof DOC_DRIFT_SEVERITIES)[number];
14
14
  export type DocDriftStatus = (typeof DOC_DRIFT_STATUSES)[number];
15
15
 
16
- export const DocDriftFindingSchema = Type.Object(
17
- {
18
- file: Type.String({ minLength: 1 }),
19
- description: Type.String({ minLength: 1 }),
20
- severity: Type.Union(
21
- DOC_DRIFT_SEVERITIES.map((value) => Type.Literal(value)),
22
- ),
23
- relatedFiles: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
24
- },
25
- { additionalProperties: false },
26
- );
16
+ export const DocDriftFindingSchema = z.object({
17
+ file: z.string().min(1),
18
+ description: z.string().min(1),
19
+ severity: z.enum(DOC_DRIFT_SEVERITIES),
20
+ relatedFiles: z.array(z.string().min(1)).optional(),
21
+ }).strict();
27
22
 
28
- export const DocDriftOutputSchema = Type.Object(
29
- {
30
- findings: Type.Array(DocDriftFindingSchema),
31
- status: Type.Union(
32
- DOC_DRIFT_STATUSES.map((value) => Type.Literal(value)),
33
- ),
34
- },
35
- { additionalProperties: false },
36
- );
23
+ export const DocDriftOutputSchema = z.object({
24
+ findings: z.array(DocDriftFindingSchema),
25
+ status: z.enum(DOC_DRIFT_STATUSES),
26
+ }).strict();
37
27
 
38
- export type DocDriftFinding = Static<typeof DocDriftFindingSchema>;
39
- export type DocDriftOutput = Static<typeof DocDriftOutputSchema>;
28
+ export type DocDriftFinding = z.infer<typeof DocDriftFindingSchema>;
29
+ export type DocDriftOutput = z.infer<typeof DocDriftOutputSchema>;
@@ -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 prompt = buildAssessmentPrompt({
97
- schemaText,
98
- comments: input.comments,
99
- repo: input.repo,
100
- prNumber: input.prNumber,
101
- selectedTargetLabel: input.selectedTargetLabel,
102
- });
103
-
104
- return runWithOutputValidation<FixPrAssessmentBatch>(
105
- input.createAgentSession as any,
106
- {
107
- cwd: input.cwd,
108
- prompt,
109
- schema: schemaText,
110
- parse: (raw) =>
111
- parseStructuredOutput<FixPrAssessmentBatch>(raw, FixPrAssessmentBatchSchema),
112
- model: input.model,
113
- thinkingLevel: input.thinkingLevel ?? null,
114
- maxAttempts: input.maxAttempts,
115
- reliability: input.paths
116
- ? { paths: input.paths, cwd: input.cwd, command: "fix-pr", operation: "assessment" }
117
- : undefined,
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
  /**
@@ -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 { Type, type Static } from "@sinclair/typebox";
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 = Type.Object(
14
- {
15
- commentId: Type.Integer(),
16
- verdict: Type.Union(
17
- FIX_PR_ASSESSMENT_VERDICTS.map((value) => Type.Literal(value)),
18
- ),
19
- rationale: Type.String({ minLength: 1 }),
20
- affectedFiles: Type.Array(Type.String({ minLength: 1 })),
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 = Type.Object(
28
- {
29
- assessments: Type.Array(FixPrCommentAssessmentSchema),
30
- summary: Type.Optional(Type.String()),
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 = Static<typeof FixPrCommentAssessmentSchema>;
36
- export type FixPrAssessmentBatch = Static<typeof FixPrAssessmentBatchSchema>;
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 \"fix: address PR review comments (iteration ${iteration})\"\``,
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 \"${scriptsDir}/trigger-review.ts\" \"${repo}\" ${prNumber} \"${reviewer.type}\" \"${reviewer.triggerMethod}\"\``,
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 \"${scriptsDir}/wait-and-check.ts\" \"${sessionDir}\" ${delay} ${iteration + 1} \"${repo}\" ${prNumber}`,
205
+ `bun "${scriptsDir}/wait-and-check.ts" "${sessionDir}" ${delay} ${iteration + 1} "${repo}" ${prNumber}`,
196
206
  "```",
197
- `${reviewer.type !== "none" ? "5" : "4"}. Read the last line of output:`,
198
- ` - If \`hasNewComments: true\` and iteration < ${maxIter}: process the new comments (go back to Step 1)`,
199
- ` - If \`hasNewComments: false\` or iteration >= ${maxIter}: report done`,
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 { type Static, Type } from "@sinclair/typebox";
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 = Type.Object(
14
- {
15
- type: Type.Union(VALID_COMMIT_TYPES.map((value) => Type.Literal(value))),
16
- scope: Type.Union([Type.String(), Type.Null()]),
17
- summary: Type.String({ minLength: 1 }),
18
- details: Type.Array(Type.String()),
19
- files: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
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 = Type.Object(
25
- {
26
- commits: Type.Array(CommitGroupSchema, { minItems: 1 }),
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 = Static<typeof CommitGroupSchema>;
32
- export type CommitPlan = Static<typeof CommitPlanSchema>;
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