supipowers 1.2.6 → 1.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.
Files changed (137) hide show
  1. package/README.md +118 -56
  2. package/bin/install.ts +48 -128
  3. package/package.json +11 -3
  4. package/skills/code-review/SKILL.md +137 -40
  5. package/skills/context-mode/SKILL.md +67 -56
  6. package/skills/creating-supi-agents/SKILL.md +204 -0
  7. package/skills/debugging/SKILL.md +86 -40
  8. package/skills/fix-pr/SKILL.md +96 -65
  9. package/skills/planning/SKILL.md +103 -46
  10. package/skills/qa-strategy/SKILL.md +68 -46
  11. package/skills/receiving-code-review/SKILL.md +60 -53
  12. package/skills/release/SKILL.md +111 -39
  13. package/skills/tdd/SKILL.md +118 -67
  14. package/skills/verification/SKILL.md +71 -37
  15. package/src/bootstrap.ts +27 -5
  16. package/src/commands/agents.ts +249 -0
  17. package/src/commands/ai-review.ts +1113 -0
  18. package/src/commands/config.ts +224 -95
  19. package/src/commands/doctor.ts +19 -13
  20. package/src/commands/fix-pr.ts +8 -11
  21. package/src/commands/generate.ts +200 -0
  22. package/src/commands/model-picker.ts +5 -15
  23. package/src/commands/model.ts +4 -5
  24. package/src/commands/optimize-context.ts +202 -0
  25. package/src/commands/plan.ts +148 -92
  26. package/src/commands/qa.ts +14 -23
  27. package/src/commands/release.ts +504 -275
  28. package/src/commands/review.ts +643 -86
  29. package/src/commands/status.ts +44 -17
  30. package/src/commands/supi.ts +69 -41
  31. package/src/commands/update.ts +57 -2
  32. package/src/config/defaults.ts +6 -39
  33. package/src/config/loader.ts +388 -40
  34. package/src/config/model-resolver.ts +26 -22
  35. package/src/config/schema.ts +113 -48
  36. package/src/context/analyzer.ts +61 -2
  37. package/src/context/optimizer.ts +199 -0
  38. package/src/context-mode/compressor.ts +14 -11
  39. package/src/context-mode/detector.ts +16 -54
  40. package/src/context-mode/event-extractor.ts +45 -16
  41. package/src/context-mode/event-store.ts +225 -16
  42. package/src/context-mode/hooks.ts +195 -22
  43. package/src/context-mode/knowledge/chunker.ts +235 -0
  44. package/src/context-mode/knowledge/store.ts +187 -0
  45. package/src/context-mode/routing.ts +12 -23
  46. package/src/context-mode/sandbox/executor.ts +183 -0
  47. package/src/context-mode/sandbox/runners.ts +40 -0
  48. package/src/context-mode/snapshot-builder.ts +243 -7
  49. package/src/context-mode/tools.ts +440 -0
  50. package/src/context-mode/web/fetcher.ts +117 -0
  51. package/src/context-mode/web/html-to-md.ts +293 -0
  52. package/src/debug/logger.ts +107 -0
  53. package/src/deps/registry.ts +0 -20
  54. package/src/docs/drift.ts +454 -0
  55. package/src/fix-pr/fetch-comments.ts +66 -0
  56. package/src/git/commit-msg.ts +2 -1
  57. package/src/git/commit.ts +123 -141
  58. package/src/git/conventions.ts +2 -2
  59. package/src/git/status.ts +4 -1
  60. package/src/lsp/bridge.ts +138 -12
  61. package/src/planning/approval-flow.ts +125 -19
  62. package/src/planning/plan-writer-prompt.ts +4 -11
  63. package/src/planning/planning-ask-tool.ts +81 -0
  64. package/src/planning/prompt-builder.ts +9 -169
  65. package/src/planning/system-prompt.ts +290 -0
  66. package/src/platform/omp.ts +50 -4
  67. package/src/platform/progress.ts +182 -0
  68. package/src/platform/test-utils.ts +4 -1
  69. package/src/platform/tui-colors.ts +30 -0
  70. package/src/platform/types.ts +1 -0
  71. package/src/qa/detect-app-type.ts +102 -0
  72. package/src/qa/discover-routes.ts +353 -0
  73. package/src/quality/ai-session.ts +96 -0
  74. package/src/quality/ai-setup.ts +86 -0
  75. package/src/quality/gates/ai-review.ts +129 -0
  76. package/src/quality/gates/build.ts +8 -0
  77. package/src/quality/gates/command.ts +150 -0
  78. package/src/quality/gates/format.ts +28 -0
  79. package/src/quality/gates/lint.ts +22 -0
  80. package/src/quality/gates/lsp-diagnostics.ts +84 -0
  81. package/src/quality/gates/test-suite.ts +8 -0
  82. package/src/quality/gates/typecheck.ts +22 -0
  83. package/src/quality/registry.ts +25 -0
  84. package/src/quality/review-gates.ts +33 -0
  85. package/src/quality/runner.ts +268 -0
  86. package/src/quality/schemas.ts +48 -0
  87. package/src/quality/setup.ts +227 -0
  88. package/src/release/changelog.ts +7 -3
  89. package/src/release/channels/custom.ts +43 -0
  90. package/src/release/channels/gitea.ts +35 -0
  91. package/src/release/channels/github.ts +35 -0
  92. package/src/release/channels/gitlab.ts +35 -0
  93. package/src/release/channels/registry.ts +52 -0
  94. package/src/release/channels/types.ts +27 -0
  95. package/src/release/detector.ts +10 -63
  96. package/src/release/executor.ts +61 -51
  97. package/src/release/prompt.ts +38 -38
  98. package/src/release/version.ts +129 -10
  99. package/src/review/agent-loader.ts +331 -0
  100. package/src/review/consolidator.ts +180 -0
  101. package/src/review/default-agents/correctness.md +72 -0
  102. package/src/review/default-agents/maintainability.md +64 -0
  103. package/src/review/default-agents/security.md +67 -0
  104. package/src/review/fixer.ts +219 -0
  105. package/src/review/multi-agent-runner.ts +135 -0
  106. package/src/review/output.ts +147 -0
  107. package/src/review/prompts/agent-review-wrapper.md +36 -0
  108. package/src/review/prompts/fix-findings.md +32 -0
  109. package/src/review/prompts/fix-output-schema.md +18 -0
  110. package/src/review/prompts/invalid-output-retry.md +22 -0
  111. package/src/review/prompts/output-instructions.md +14 -0
  112. package/src/review/prompts/review-output-schema.md +38 -0
  113. package/src/review/prompts/single-review.md +53 -0
  114. package/src/review/prompts/validation-review.md +30 -0
  115. package/src/review/runner.ts +128 -0
  116. package/src/review/scope.ts +353 -0
  117. package/src/review/template.ts +15 -0
  118. package/src/review/types.ts +296 -0
  119. package/src/review/validator.ts +160 -0
  120. package/src/storage/plans.ts +5 -3
  121. package/src/storage/reports.ts +50 -7
  122. package/src/storage/review-sessions.ts +117 -0
  123. package/src/text.ts +19 -0
  124. package/src/types.ts +336 -26
  125. package/src/utils/paths.ts +39 -0
  126. package/src/visual/companion.ts +5 -3
  127. package/src/visual/start-server.ts +101 -0
  128. package/src/visual/stop-server.ts +39 -0
  129. package/bin/ctx-mode-wrapper.mjs +0 -66
  130. package/src/config/profiles.ts +0 -64
  131. package/src/context-mode/installer.ts +0 -38
  132. package/src/quality/ai-review-gate.ts +0 -43
  133. package/src/quality/gate-runner.ts +0 -67
  134. package/src/quality/lsp-gate.ts +0 -24
  135. package/src/quality/test-gate.ts +0 -39
  136. package/src/visual/scripts/start-server.sh +0 -98
  137. package/src/visual/scripts/stop-server.sh +0 -21
@@ -0,0 +1,296 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { TSchema } from "@sinclair/typebox";
3
+ import { Value } from "@sinclair/typebox/value";
4
+ import type {
5
+ ConfiguredReviewAgent,
6
+ ReviewAgentConfig,
7
+ ReviewAgentDefinition,
8
+ ReviewAgentsConfig,
9
+ ReviewFinding,
10
+ ReviewFixOutput,
11
+ ReviewFixRecord,
12
+ ReviewIterationSummary,
13
+ ReviewOutput,
14
+ ReviewPostConsolidationAction,
15
+ ReviewScope,
16
+ ReviewScopeFile,
17
+ ReviewScopeStats,
18
+ ReviewSession,
19
+ ReviewSessionArtifacts,
20
+ } from "../types.js";
21
+ export type {
22
+ ConfiguredReviewAgent,
23
+ ReviewAgentConfig,
24
+ ReviewAgentDefinition,
25
+ ReviewAgentsConfig,
26
+ ReviewFinding,
27
+ ReviewFixOutput,
28
+ ReviewFixRecord,
29
+ ReviewIterationSummary,
30
+ ReviewOutput,
31
+ ReviewPostConsolidationAction,
32
+ ReviewScope,
33
+ ReviewScopeFile,
34
+ ReviewScopeStats,
35
+ ReviewSession,
36
+ ReviewSessionArtifacts,
37
+ } from "../types.js";
38
+ export const REVIEW_LEVELS = ["quick", "deep", "multi-agent"] as const;
39
+ export const REVIEW_SCOPE_MODES = ["pull-request", "uncommitted", "commit", "custom"] as const;
40
+ export const REVIEW_OUTPUT_STATUSES = ["passed", "failed", "blocked"] as const;
41
+ export const REVIEW_FINDING_SEVERITIES = ["error", "warning", "info"] as const;
42
+ export const REVIEW_FINDING_PRIORITIES = ["P0", "P1", "P2", "P3"] as const;
43
+ export const REVIEW_VALIDATION_VERDICTS = ["confirmed", "rejected", "uncertain"] as const;
44
+ export const REVIEW_SESSION_STATUSES = ["running", "completed", "blocked", "cancelled"] as const;
45
+ export const REVIEW_POST_CONSOLIDATION_ACTIONS = ["fix-now", "document-only", "discuss-before-fixing"] as const;
46
+ export const REVIEW_FIX_STATUSES = ["applied", "skipped", "failed"] as const;
47
+
48
+ export const ReviewScopeFileSchema = Type.Object(
49
+ {
50
+ path: Type.String({ minLength: 1 }),
51
+ additions: Type.Number({ minimum: 0 }),
52
+ deletions: Type.Number({ minimum: 0 }),
53
+ diff: Type.String(),
54
+ },
55
+ { additionalProperties: false },
56
+ );
57
+
58
+ export const ReviewScopeStatsSchema = Type.Object(
59
+ {
60
+ filesChanged: Type.Number({ minimum: 0 }),
61
+ excludedFiles: Type.Number({ minimum: 0 }),
62
+ additions: Type.Number({ minimum: 0 }),
63
+ deletions: Type.Number({ minimum: 0 }),
64
+ },
65
+ { additionalProperties: false },
66
+ );
67
+
68
+ export const ReviewScopeSchema = Type.Object(
69
+ {
70
+ mode: Type.Union(REVIEW_SCOPE_MODES.map((mode) => Type.Literal(mode))),
71
+ description: Type.String({ minLength: 1 }),
72
+ diff: Type.String(),
73
+ files: Type.Array(ReviewScopeFileSchema),
74
+ stats: ReviewScopeStatsSchema,
75
+ baseBranch: Type.Optional(Type.String({ minLength: 1 })),
76
+ commit: Type.Optional(Type.String({ minLength: 1 })),
77
+ customInstructions: Type.Optional(Type.String({ minLength: 1 })),
78
+ },
79
+ { additionalProperties: false },
80
+ );
81
+
82
+ export const ReviewFindingValidationSchema = Type.Object(
83
+ {
84
+ verdict: Type.Union(REVIEW_VALIDATION_VERDICTS.map((value) => Type.Literal(value))),
85
+ reasoning: Type.String({ minLength: 1 }),
86
+ validatedBy: Type.String({ minLength: 1 }),
87
+ validatedAt: Type.String({ minLength: 1 }),
88
+ },
89
+ { additionalProperties: false },
90
+ );
91
+
92
+ export const ReviewFindingSchema = Type.Object(
93
+ {
94
+ id: Type.String({ minLength: 1 }),
95
+ title: Type.String({ minLength: 1 }),
96
+ severity: Type.Union(REVIEW_FINDING_SEVERITIES.map((value) => Type.Literal(value))),
97
+ priority: Type.Union(REVIEW_FINDING_PRIORITIES.map((value) => Type.Literal(value))),
98
+ confidence: Type.Number({ minimum: 0, maximum: 1 }),
99
+ file: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
100
+ lineStart: Type.Union([Type.Number({ minimum: 1 }), Type.Null()]),
101
+ lineEnd: Type.Union([Type.Number({ minimum: 1 }), Type.Null()]),
102
+ body: Type.String({ minLength: 1 }),
103
+ suggestion: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
104
+ agent: Type.Optional(Type.String({ minLength: 1 })),
105
+ validation: Type.Optional(ReviewFindingValidationSchema),
106
+ },
107
+ { additionalProperties: false },
108
+ );
109
+
110
+ export const ReviewOutputSchema = Type.Object(
111
+ {
112
+ findings: Type.Array(ReviewFindingSchema),
113
+ summary: Type.String({ minLength: 1 }),
114
+ status: Type.Union(REVIEW_OUTPUT_STATUSES.map((value) => Type.Literal(value))),
115
+ },
116
+ { additionalProperties: false },
117
+ );
118
+
119
+ export const ReviewAgentConfigSchema = Type.Object(
120
+ {
121
+ name: Type.String({ minLength: 1 }),
122
+ enabled: Type.Boolean(),
123
+ data: Type.String({ minLength: 1 }),
124
+ model: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
125
+ },
126
+ { additionalProperties: false },
127
+ );
128
+
129
+ export const ReviewAgentsConfigSchema = Type.Object(
130
+ {
131
+ agents: Type.Array(ReviewAgentConfigSchema),
132
+ },
133
+ { additionalProperties: false },
134
+ );
135
+
136
+ export const ReviewAgentFrontmatterSchema = Type.Object(
137
+ {
138
+ name: Type.String({ minLength: 1 }),
139
+ description: Type.String({ minLength: 1 }),
140
+ focus: Type.Optional(Type.String({ minLength: 1 })),
141
+ },
142
+ { additionalProperties: false },
143
+ );
144
+
145
+ export const ReviewIterationSummarySchema = Type.Object(
146
+ {
147
+ iteration: Type.Number({ minimum: 1 }),
148
+ findings: Type.Number({ minimum: 0 }),
149
+ status: Type.Union(REVIEW_OUTPUT_STATUSES.map((value) => Type.Literal(value))),
150
+ file: Type.String({ minLength: 1 }),
151
+ createdAt: Type.String({ minLength: 1 }),
152
+ },
153
+ { additionalProperties: false },
154
+ );
155
+
156
+ export const ReviewFixRecordSchema = Type.Object(
157
+ {
158
+ findingIds: Type.Array(Type.String({ minLength: 1 })),
159
+ file: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
160
+ status: Type.Union(REVIEW_FIX_STATUSES.map((value) => Type.Literal(value))),
161
+ summary: Type.String({ minLength: 1 }),
162
+ },
163
+ { additionalProperties: false },
164
+ );
165
+
166
+ export const REVIEW_FIX_OUTPUT_STATUSES = ["applied", "partial", "skipped", "blocked"] as const;
167
+
168
+ export const ReviewFixOutputSchema = Type.Object(
169
+ {
170
+ fixes: Type.Array(ReviewFixRecordSchema),
171
+ summary: Type.String({ minLength: 1 }),
172
+ status: Type.Union(REVIEW_FIX_OUTPUT_STATUSES.map((value) => Type.Literal(value))),
173
+ },
174
+ { additionalProperties: false },
175
+ );
176
+
177
+
178
+ export const ReviewSessionArtifactsSchema = Type.Object(
179
+ {
180
+ scope: Type.String({ minLength: 1 }),
181
+ iterationsDir: Type.String({ minLength: 1 }),
182
+ agentsDir: Type.String({ minLength: 1 }),
183
+ rawFindings: Type.Optional(Type.String({ minLength: 1 })),
184
+ validatedFindings: Type.Optional(Type.String({ minLength: 1 })),
185
+ consolidatedFindings: Type.Optional(Type.String({ minLength: 1 })),
186
+ findingsReport: Type.Optional(Type.String({ minLength: 1 })),
187
+ },
188
+ { additionalProperties: false },
189
+ );
190
+
191
+ export const ReviewSessionSchema = Type.Object(
192
+ {
193
+ id: Type.String({ minLength: 1 }),
194
+ createdAt: Type.String({ minLength: 1 }),
195
+ updatedAt: Type.String({ minLength: 1 }),
196
+ level: Type.Union(REVIEW_LEVELS.map((value) => Type.Literal(value))),
197
+ status: Type.Union(REVIEW_SESSION_STATUSES.map((value) => Type.Literal(value))),
198
+ scope: ReviewScopeSchema,
199
+ validateFindings: Type.Boolean(),
200
+ consolidate: Type.Boolean(),
201
+ postConsolidationAction: Type.Union([
202
+ Type.Union(REVIEW_POST_CONSOLIDATION_ACTIONS.map((value) => Type.Literal(value))),
203
+ Type.Null(),
204
+ ]),
205
+ maxIterations: Type.Number({ minimum: 0 }),
206
+ currentIteration: Type.Number({ minimum: 0 }),
207
+ iterations: Type.Array(ReviewIterationSummarySchema),
208
+ fixes: Type.Array(ReviewFixRecordSchema),
209
+ artifacts: ReviewSessionArtifactsSchema,
210
+ agents: Type.Array(Type.String({ minLength: 1 })),
211
+ },
212
+ { additionalProperties: false },
213
+ );
214
+
215
+ export interface ReviewValidationError {
216
+ path: string;
217
+ message: string;
218
+ }
219
+
220
+ function normalizeErrorPath(path: string): string {
221
+ return path.replace(/^\//, "").replace(/\//g, ".") || "(root)";
222
+ }
223
+
224
+ export function collectReviewValidationErrors(schema: TSchema, data: unknown): ReviewValidationError[] {
225
+ return [...Value.Errors(schema, data)].map((error) => ({
226
+ path: normalizeErrorPath(error.path),
227
+ message: error.message,
228
+ }));
229
+ }
230
+
231
+ export function formatReviewValidationErrors(errors: ReviewValidationError[]): string[] {
232
+ return errors.map((error) => `${error.path}: ${error.message}`);
233
+ }
234
+
235
+ export function isReviewScopeFile(value: unknown): value is ReviewScopeFile {
236
+ return Value.Check(ReviewScopeFileSchema, value);
237
+ }
238
+
239
+ export function isReviewScopeStats(value: unknown): value is ReviewScopeStats {
240
+ return Value.Check(ReviewScopeStatsSchema, value);
241
+ }
242
+
243
+ export function isReviewScope(value: unknown): value is ReviewScope {
244
+ return Value.Check(ReviewScopeSchema, value);
245
+ }
246
+
247
+ export function isReviewFinding(value: unknown): value is ReviewFinding {
248
+ return Value.Check(ReviewFindingSchema, value);
249
+ }
250
+
251
+ export function isReviewOutput(value: unknown): value is ReviewOutput {
252
+ return Value.Check(ReviewOutputSchema, value);
253
+ }
254
+
255
+ export function isReviewAgentConfig(value: unknown): value is ReviewAgentConfig {
256
+ return Value.Check(ReviewAgentConfigSchema, value);
257
+ }
258
+
259
+ export function isReviewAgentsConfig(value: unknown): value is ReviewAgentsConfig {
260
+ return Value.Check(ReviewAgentsConfigSchema, value);
261
+ }
262
+
263
+ export function isReviewSessionArtifacts(value: unknown): value is ReviewSessionArtifacts {
264
+ return Value.Check(ReviewSessionArtifactsSchema, value);
265
+ }
266
+
267
+ export function isReviewIterationSummary(value: unknown): value is ReviewIterationSummary {
268
+ return Value.Check(ReviewIterationSummarySchema, value);
269
+ }
270
+
271
+ export function isReviewFixRecord(value: unknown): value is ReviewFixRecord {
272
+ return Value.Check(ReviewFixRecordSchema, value);
273
+ }
274
+
275
+ export function isReviewSession(value: unknown): value is ReviewSession {
276
+ return Value.Check(ReviewSessionSchema, value);
277
+ }
278
+
279
+ export function isReviewAgentDefinition(value: unknown): value is ReviewAgentDefinition {
280
+ if (typeof value !== "object" || value === null) {
281
+ return false;
282
+ }
283
+
284
+ const candidate = value as ReviewAgentDefinition;
285
+ return (
286
+ typeof candidate.name === "string" &&
287
+ candidate.name.length > 0 &&
288
+ typeof candidate.description === "string" &&
289
+ candidate.description.length > 0 &&
290
+ (candidate.focus === null || typeof candidate.focus === "string") &&
291
+ typeof candidate.prompt === "string" &&
292
+ candidate.prompt.length > 0 &&
293
+ typeof candidate.filePath === "string" &&
294
+ candidate.filePath.length > 0
295
+ );
296
+ }
@@ -0,0 +1,160 @@
1
+ import reviewOutputSchema from "./prompts/review-output-schema.md" with { type: "text" };
2
+ import validationReviewPrompt from "./prompts/validation-review.md" with { type: "text" };
3
+ import type { GateExecutionContext, ReviewFinding, ReviewOutput, ReviewScope } from "../types.js";
4
+ import { explainReviewOutputFailure, parseReviewOutput, runWithOutputValidation } from "./output.js";
5
+ import { renderReviewTemplate } from "./template.js";
6
+
7
+ export interface ReviewValidationInput {
8
+ cwd: string;
9
+ scope: ReviewScope;
10
+ findings: ReviewFinding[];
11
+ createAgentSession: GateExecutionContext["createAgentSession"];
12
+ model?: string;
13
+ thinkingLevel?: string | null;
14
+ timeoutMs?: number;
15
+ validatorName?: string;
16
+ now?: () => Date;
17
+ }
18
+
19
+ export interface ReviewValidationResult {
20
+ output: ReviewOutput;
21
+ attempts: number;
22
+ rawOutputs: string[];
23
+ }
24
+
25
+ function summarizeValidation(findings: ReviewFinding[]): string {
26
+ const counts = findings.reduce(
27
+ (summary, finding) => {
28
+ const verdict = finding.validation?.verdict ?? "uncertain";
29
+ summary[verdict] += 1;
30
+ return summary;
31
+ },
32
+ { confirmed: 0, rejected: 0, uncertain: 0 },
33
+ );
34
+
35
+ return `Validation complete: ${counts.confirmed} confirmed, ${counts.rejected} rejected, ${counts.uncertain} uncertain.`;
36
+ }
37
+
38
+ function statusFromValidation(findings: ReviewFinding[]): ReviewOutput["status"] {
39
+ const confirmed = findings.filter((finding) => finding.validation?.verdict === "confirmed").length;
40
+ const uncertain = findings.filter((finding) => finding.validation?.verdict === "uncertain").length;
41
+
42
+ if (confirmed > 0) {
43
+ return "failed";
44
+ }
45
+ if (uncertain > 0) {
46
+ return "blocked";
47
+ }
48
+ return "passed";
49
+ }
50
+
51
+ function applyValidationResults(
52
+ findings: ReviewFinding[],
53
+ candidate: ReviewFinding[],
54
+ validatorName: string,
55
+ validatedAt: string,
56
+ fallbackReason: string,
57
+ ): ReviewFinding[] {
58
+ const byId = new Map(candidate.map((finding) => [finding.id, finding]));
59
+
60
+ return findings.map((finding) => {
61
+ const validated = byId.get(finding.id);
62
+ const verdict = validated?.validation?.verdict ?? "uncertain";
63
+ const reasoning = validated?.validation?.reasoning?.trim() || fallbackReason;
64
+
65
+ return {
66
+ ...finding,
67
+ validation: {
68
+ verdict,
69
+ reasoning,
70
+ validatedBy: validatorName,
71
+ validatedAt,
72
+ },
73
+ };
74
+ });
75
+ }
76
+
77
+ export function buildValidationPrompt(
78
+ scope: ReviewScope,
79
+ findings: ReviewFinding[],
80
+ validatorName: string,
81
+ validatedAt: string,
82
+ ): string {
83
+ return renderReviewTemplate(validationReviewPrompt, {
84
+ scope,
85
+ findingsJson: JSON.stringify(findings, null, 2),
86
+ validatorName,
87
+ validatedAt,
88
+ outputSchema: reviewOutputSchema.trim(),
89
+ });
90
+ }
91
+
92
+ export async function validateReviewFindings(input: ReviewValidationInput): Promise<ReviewValidationResult> {
93
+ if (input.findings.length === 0) {
94
+ return {
95
+ output: {
96
+ findings: [],
97
+ summary: "No findings to validate.",
98
+ status: "passed",
99
+ },
100
+ attempts: 0,
101
+ rawOutputs: [],
102
+ };
103
+ }
104
+
105
+ const validatorName = input.validatorName ?? "validator";
106
+ const validatedAt = (input.now ?? (() => new Date()))().toISOString();
107
+ const result = await runWithOutputValidation(input.createAgentSession, {
108
+ cwd: input.cwd,
109
+ prompt: buildValidationPrompt(input.scope, input.findings, validatorName, validatedAt),
110
+ schema: reviewOutputSchema.trim(),
111
+ parse(raw) {
112
+ const output = parseReviewOutput(raw);
113
+ return {
114
+ output,
115
+ error: output ? null : explainReviewOutputFailure(raw),
116
+ };
117
+ },
118
+ model: input.model,
119
+ thinkingLevel: input.thinkingLevel ?? null,
120
+ timeoutMs: input.timeoutMs ?? 120_000,
121
+ });
122
+
123
+ if (result.status === "blocked") {
124
+ const findings = applyValidationResults(
125
+ input.findings,
126
+ [],
127
+ validatorName,
128
+ validatedAt,
129
+ result.error,
130
+ );
131
+
132
+ return {
133
+ output: {
134
+ findings,
135
+ summary: result.error,
136
+ status: "blocked",
137
+ },
138
+ attempts: result.attempts,
139
+ rawOutputs: result.rawOutputs,
140
+ };
141
+ }
142
+
143
+ const findings = applyValidationResults(
144
+ input.findings,
145
+ result.output.findings,
146
+ validatorName,
147
+ validatedAt,
148
+ "Validator did not return a verdict for this finding.",
149
+ );
150
+
151
+ return {
152
+ output: {
153
+ findings,
154
+ summary: summarizeValidation(findings),
155
+ status: statusFromValidation(findings),
156
+ },
157
+ attempts: result.attempts,
158
+ rawOutputs: [result.rawOutput],
159
+ };
160
+ }
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { normalizeLineEndings } from "../text.js";
3
4
  import type { Plan, PlanTask, TaskComplexity } from "../types.js";
4
5
  import type { PlatformPaths } from "../platform/types.js";
5
6
 
@@ -36,7 +37,8 @@ export function savePlan(paths: PlatformPaths, cwd: string, filename: string, co
36
37
 
37
38
  /** Parse a plan markdown file into a Plan object */
38
39
  export function parsePlan(content: string, filePath: string): Plan {
39
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
40
+ const normalizedContent = normalizeLineEndings(content);
41
+ const frontmatterMatch = normalizedContent.match(/^---\n([\s\S]*?)\n---/);
40
42
  const meta: Record<string, string | string[]> = {};
41
43
 
42
44
  if (frontmatterMatch) {
@@ -56,13 +58,13 @@ export function parsePlan(content: string, filePath: string): Plan {
56
58
  }
57
59
  }
58
60
 
59
- const tasks = parseTasksFromMarkdown(content);
61
+ const tasks = parseTasksFromMarkdown(normalizedContent);
60
62
 
61
63
  return {
62
64
  name: (meta.name as string) ?? path.basename(filePath, ".md"),
63
65
  created: (meta.created as string) ?? "",
64
66
  tags: (meta.tags as string[]) ?? [],
65
- context: extractContext(content),
67
+ context: extractContext(normalizedContent),
66
68
  tasks,
67
69
  filePath,
68
70
  };
@@ -1,12 +1,47 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { ReviewReport } from "../types.js";
3
+ import type { GateResult, GateStatus, ReviewReport } from "../types.js";
4
4
  import type { PlatformPaths } from "../platform/types.js";
5
5
 
6
6
  function getReportsDir(paths: PlatformPaths, cwd: string): string {
7
7
  return paths.project(cwd, "reports");
8
8
  }
9
9
 
10
+ function isGateStatus(value: unknown): value is GateStatus {
11
+ return value === "passed" || value === "failed" || value === "skipped" || value === "blocked";
12
+ }
13
+
14
+ function isGateResult(value: unknown): value is GateResult {
15
+ return (
16
+ typeof value === "object" &&
17
+ value !== null &&
18
+ typeof (value as GateResult).gate === "string" &&
19
+ isGateStatus((value as GateResult).status) &&
20
+ typeof (value as GateResult).summary === "string" &&
21
+ Array.isArray((value as GateResult).issues)
22
+ );
23
+ }
24
+
25
+ function isReviewReport(value: unknown): value is ReviewReport {
26
+ return (
27
+ typeof value === "object" &&
28
+ value !== null &&
29
+ typeof (value as ReviewReport).timestamp === "string" &&
30
+ Array.isArray((value as ReviewReport).selectedGates) &&
31
+ Array.isArray((value as ReviewReport).gates) &&
32
+ (value as ReviewReport).gates.every(isGateResult) &&
33
+ typeof (value as ReviewReport).summary === "object" &&
34
+ (value as ReviewReport).summary !== null &&
35
+ typeof (value as ReviewReport).summary.passed === "number" &&
36
+ typeof (value as ReviewReport).summary.failed === "number" &&
37
+ typeof (value as ReviewReport).summary.skipped === "number" &&
38
+ typeof (value as ReviewReport).summary.blocked === "number" &&
39
+ ((value as ReviewReport).overallStatus === "passed" ||
40
+ (value as ReviewReport).overallStatus === "failed" ||
41
+ (value as ReviewReport).overallStatus === "blocked")
42
+ );
43
+ }
44
+
10
45
  /** Save a review report */
11
46
  export function saveReviewReport(paths: PlatformPaths, cwd: string, report: ReviewReport): string {
12
47
  const dir = getReportsDir(paths, cwd);
@@ -21,15 +56,23 @@ export function saveReviewReport(paths: PlatformPaths, cwd: string, report: Revi
21
56
  export function loadLatestReport(paths: PlatformPaths, cwd: string): ReviewReport | null {
22
57
  const dir = getReportsDir(paths, cwd);
23
58
  if (!fs.existsSync(dir)) return null;
59
+
24
60
  const files = fs
25
61
  .readdirSync(dir)
26
- .filter((f) => f.startsWith("review-") && f.endsWith(".json"))
62
+ .filter((file) => file.startsWith("review-") && file.endsWith(".json"))
27
63
  .sort()
28
64
  .reverse();
29
- if (files.length === 0) return null;
30
- try {
31
- return JSON.parse(fs.readFileSync(path.join(dir, files[0]), "utf-8"));
32
- } catch {
33
- return null;
65
+
66
+ for (const file of files) {
67
+ try {
68
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, file), "utf-8"));
69
+ if (isReviewReport(parsed)) {
70
+ return parsed;
71
+ }
72
+ } catch {
73
+ // Ignore malformed report files and continue to older entries.
74
+ }
34
75
  }
76
+
77
+ return null;
35
78
  }
@@ -0,0 +1,117 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { PlatformPaths } from "../platform/types.js";
4
+ import type { ReviewSession } from "../types.js";
5
+ import { isReviewSession } from "../review/types.js";
6
+
7
+ const SESSIONS_DIR = "reviews";
8
+ const ITERATIONS_DIR = "iterations";
9
+ const AGENTS_DIR = "agents";
10
+
11
+ function getSessionsDir(paths: PlatformPaths, cwd: string): string {
12
+ return paths.project(cwd, SESSIONS_DIR);
13
+ }
14
+
15
+ function ensureLayout(sessionDir: string): void {
16
+ fs.mkdirSync(path.join(sessionDir, ITERATIONS_DIR), { recursive: true });
17
+ fs.mkdirSync(path.join(sessionDir, AGENTS_DIR), { recursive: true });
18
+ }
19
+
20
+ function getLedgerPath(paths: PlatformPaths, cwd: string, sessionId: string): string {
21
+ return path.join(getReviewSessionDir(paths, cwd, sessionId), "session.json");
22
+ }
23
+
24
+ function resolveArtifactPath(sessionDir: string, relativePath: string): string {
25
+ if (!relativePath || path.isAbsolute(relativePath)) {
26
+ throw new Error("Artifact path must be a non-empty relative path.");
27
+ }
28
+
29
+ const resolved = path.resolve(sessionDir, relativePath);
30
+ const normalizedBase = path.resolve(sessionDir);
31
+ if (resolved !== normalizedBase && !resolved.startsWith(`${normalizedBase}${path.sep}`)) {
32
+ throw new Error("Artifact path must stay within the review session directory.");
33
+ }
34
+
35
+ return resolved;
36
+ }
37
+
38
+ export function getReviewSessionDir(paths: PlatformPaths, cwd: string, sessionId: string): string {
39
+ return path.join(getSessionsDir(paths, cwd), sessionId);
40
+ }
41
+
42
+ export function generateReviewSessionId(now = new Date()): string {
43
+ const iso = now.toISOString();
44
+ const date = iso.slice(0, 10).replace(/-/g, "");
45
+ const time = iso.slice(11, 19).replace(/:/g, "");
46
+ const suffix = Math.random().toString(36).slice(2, 6);
47
+ return `review-${date}-${time}-${suffix}`;
48
+ }
49
+
50
+ export function createReviewSession(paths: PlatformPaths, cwd: string, session: ReviewSession): void {
51
+ const sessionDir = getReviewSessionDir(paths, cwd, session.id);
52
+ ensureLayout(sessionDir);
53
+ fs.writeFileSync(getLedgerPath(paths, cwd, session.id), `${JSON.stringify(session, null, 2)}\n`);
54
+ }
55
+
56
+ export function loadReviewSession(paths: PlatformPaths, cwd: string, sessionId: string): ReviewSession | null {
57
+ const ledgerPath = getLedgerPath(paths, cwd, sessionId);
58
+ if (!fs.existsSync(ledgerPath)) {
59
+ return null;
60
+ }
61
+
62
+ try {
63
+ const parsed = JSON.parse(fs.readFileSync(ledgerPath, "utf-8"));
64
+ return isReviewSession(parsed) ? parsed : null;
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ export function updateReviewSession(paths: PlatformPaths, cwd: string, session: ReviewSession): void {
71
+ session.updatedAt = new Date().toISOString();
72
+ const sessionDir = getReviewSessionDir(paths, cwd, session.id);
73
+ ensureLayout(sessionDir);
74
+ fs.writeFileSync(getLedgerPath(paths, cwd, session.id), `${JSON.stringify(session, null, 2)}\n`);
75
+ }
76
+
77
+ export function listReviewSessions(paths: PlatformPaths, cwd: string): string[] {
78
+ const sessionsDir = getSessionsDir(paths, cwd);
79
+ if (!fs.existsSync(sessionsDir)) {
80
+ return [];
81
+ }
82
+
83
+ return fs
84
+ .readdirSync(sessionsDir)
85
+ .filter((entry) => entry.startsWith("review-"))
86
+ .sort()
87
+ .reverse();
88
+ }
89
+
90
+ export function writeReviewArtifact(
91
+ paths: PlatformPaths,
92
+ cwd: string,
93
+ sessionId: string,
94
+ relativePath: string,
95
+ payload: unknown,
96
+ ): string {
97
+ const sessionDir = getReviewSessionDir(paths, cwd, sessionId);
98
+ ensureLayout(sessionDir);
99
+ const artifactPath = resolveArtifactPath(sessionDir, relativePath);
100
+ fs.mkdirSync(path.dirname(artifactPath), { recursive: true });
101
+
102
+ const content = typeof payload === "string"
103
+ ? payload
104
+ : `${JSON.stringify(payload, null, 2)}\n`;
105
+ fs.writeFileSync(artifactPath, content);
106
+ return artifactPath;
107
+ }
108
+
109
+ export function readReviewArtifact(paths: PlatformPaths, cwd: string, sessionId: string, relativePath: string): string | null {
110
+ const sessionDir = getReviewSessionDir(paths, cwd, sessionId);
111
+ const artifactPath = resolveArtifactPath(sessionDir, relativePath);
112
+ if (!fs.existsSync(artifactPath)) {
113
+ return null;
114
+ }
115
+
116
+ return fs.readFileSync(artifactPath, "utf-8");
117
+ }