@leing2021/super-pi 0.14.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/README.md +372 -0
- package/extensions/.gitkeep +0 -0
- package/extensions/ce-core/index.ts +528 -0
- package/extensions/ce-core/tools/artifact-helper.ts +73 -0
- package/extensions/ce-core/tools/ask-user-question.ts +53 -0
- package/extensions/ce-core/tools/brainstorm-dialog.ts +167 -0
- package/extensions/ce-core/tools/parallel-subagent.ts +54 -0
- package/extensions/ce-core/tools/pattern-extractor.ts +139 -0
- package/extensions/ce-core/tools/plan-diff.ts +136 -0
- package/extensions/ce-core/tools/review-router.ts +95 -0
- package/extensions/ce-core/tools/session-checkpoint.ts +212 -0
- package/extensions/ce-core/tools/session-history.ts +117 -0
- package/extensions/ce-core/tools/subagent.ts +59 -0
- package/extensions/ce-core/tools/task-splitter.ts +122 -0
- package/extensions/ce-core/tools/workflow-state.ts +80 -0
- package/extensions/ce-core/tools/worktree-manager.ts +131 -0
- package/extensions/ce-core/utils/artifact-paths.ts +23 -0
- package/extensions/ce-core/utils/name-utils.ts +8 -0
- package/package.json +53 -0
- package/skills/.gitkeep +0 -0
- package/skills/01-brainstorm/SKILL.md +120 -0
- package/skills/01-brainstorm/references/builder-mode.md +39 -0
- package/skills/01-brainstorm/references/handoff.md +8 -0
- package/skills/01-brainstorm/references/premise-challenge.md +23 -0
- package/skills/01-brainstorm/references/requirements-template.md +25 -0
- package/skills/01-brainstorm/references/startup-diagnostic.md +108 -0
- package/skills/02-plan/SKILL.md +77 -0
- package/skills/02-plan/references/ceo-review-mode.md +130 -0
- package/skills/02-plan/references/handoff.md +8 -0
- package/skills/02-plan/references/implementation-unit-template.md +25 -0
- package/skills/02-plan/references/plan-template.md +21 -0
- package/skills/03-work/SKILL.md +65 -0
- package/skills/03-work/references/handoff.md +8 -0
- package/skills/03-work/references/progress-update-format.md +17 -0
- package/skills/04-review/SKILL.md +72 -0
- package/skills/04-review/references/findings-schema.md +17 -0
- package/skills/04-review/references/handoff.md +20 -0
- package/skills/04-review/references/qa-test-mode.md +134 -0
- package/skills/04-review/references/reviewer-selection.md +24 -0
- package/skills/05-compound/SKILL.md +32 -0
- package/skills/05-compound/assets/solution-template.md +27 -0
- package/skills/05-compound/references/category-map.md +9 -0
- package/skills/05-compound/references/overlap-rules.md +7 -0
- package/skills/05-compound/references/solution-schema.yaml +27 -0
- package/skills/05-compound/references/solution-search-strategy.md +60 -0
- package/skills/06-next/SKILL.md +37 -0
- package/skills/06-next/references/recommendation-logic.md +46 -0
- package/skills/07-worktree/SKILL.md +35 -0
- package/skills/07-worktree/references/worktree-lifecycle.md +22 -0
- package/skills/08-status/SKILL.md +41 -0
- package/skills/08-status/references/artifact-locations.md +10 -0
- package/skills/09-help/SKILL.md +37 -0
- package/skills/09-help/references/workflow-sequence.md +9 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { normalizeSlug } from "../utils/name-utils"
|
|
4
|
+
|
|
5
|
+
export interface BrainstormDialogInput {
|
|
6
|
+
operation: "start" | "refine" | "summarize"
|
|
7
|
+
repoRoot: string
|
|
8
|
+
artifactPath: string
|
|
9
|
+
analysis?: string
|
|
10
|
+
questions?: string[]
|
|
11
|
+
userResponses?: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DialogState {
|
|
15
|
+
artifactPath: string
|
|
16
|
+
round: number
|
|
17
|
+
status: "in_progress" | "complete"
|
|
18
|
+
analysis: string
|
|
19
|
+
openQuestions: string[]
|
|
20
|
+
history: DialogRound[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DialogRound {
|
|
24
|
+
round: number
|
|
25
|
+
analysis: string
|
|
26
|
+
questions: string[]
|
|
27
|
+
userResponses: string[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BrainstormDialogResult {
|
|
31
|
+
artifactPath: string
|
|
32
|
+
round: number
|
|
33
|
+
status: "in_progress" | "complete"
|
|
34
|
+
analysis: string
|
|
35
|
+
openQuestions: string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function dialogDir(repoRoot: string): string {
|
|
39
|
+
return path.join(repoRoot, ".context", "compound-engineering", "dialogs")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function dialogPath(repoRoot: string, artifactPath: string): string {
|
|
43
|
+
const slug = normalizeSlug(artifactPath.replace(/\//g, "-"))
|
|
44
|
+
return path.join(dialogDir(repoRoot), `${slug}.json`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createBrainstormDialogTool() {
|
|
48
|
+
return {
|
|
49
|
+
name: "brainstorm_dialog",
|
|
50
|
+
async execute(input: BrainstormDialogInput): Promise<BrainstormDialogResult> {
|
|
51
|
+
switch (input.operation) {
|
|
52
|
+
case "start":
|
|
53
|
+
return startDialog(input)
|
|
54
|
+
case "refine":
|
|
55
|
+
return refineDialog(input)
|
|
56
|
+
case "summarize":
|
|
57
|
+
return summarizeDialog(input)
|
|
58
|
+
default:
|
|
59
|
+
throw new Error(`Unknown operation: ${input.operation}`)
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readState(repoRoot: string, artifactPath: string): Promise<DialogState | null> {
|
|
66
|
+
const filePath = dialogPath(repoRoot, artifactPath)
|
|
67
|
+
try {
|
|
68
|
+
const content = await readFile(filePath, "utf8")
|
|
69
|
+
return JSON.parse(content)
|
|
70
|
+
} catch {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function writeState(repoRoot: string, state: DialogState): Promise<void> {
|
|
76
|
+
const filePath = dialogPath(repoRoot, state.artifactPath)
|
|
77
|
+
await mkdir(path.dirname(filePath), { recursive: true })
|
|
78
|
+
await writeFile(filePath, JSON.stringify(state, null, 2), "utf8")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function startDialog(input: BrainstormDialogInput): Promise<BrainstormDialogResult> {
|
|
82
|
+
const existing = await readState(input.repoRoot, input.artifactPath)
|
|
83
|
+
if (existing) {
|
|
84
|
+
return {
|
|
85
|
+
artifactPath: existing.artifactPath,
|
|
86
|
+
round: existing.round,
|
|
87
|
+
status: existing.status,
|
|
88
|
+
analysis: existing.analysis,
|
|
89
|
+
openQuestions: existing.openQuestions,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const state: DialogState = {
|
|
94
|
+
artifactPath: input.artifactPath,
|
|
95
|
+
round: 1,
|
|
96
|
+
status: "in_progress",
|
|
97
|
+
analysis: input.analysis ?? "",
|
|
98
|
+
openQuestions: input.questions ?? [],
|
|
99
|
+
history: [
|
|
100
|
+
{
|
|
101
|
+
round: 1,
|
|
102
|
+
analysis: input.analysis ?? "",
|
|
103
|
+
questions: input.questions ?? [],
|
|
104
|
+
userResponses: [],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await writeState(input.repoRoot, state)
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
artifactPath: state.artifactPath,
|
|
113
|
+
round: state.round,
|
|
114
|
+
status: state.status,
|
|
115
|
+
analysis: state.analysis,
|
|
116
|
+
openQuestions: state.openQuestions,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function refineDialog(input: BrainstormDialogInput): Promise<BrainstormDialogResult> {
|
|
121
|
+
const existing = await readState(input.repoRoot, input.artifactPath)
|
|
122
|
+
if (!existing) {
|
|
123
|
+
throw new Error("No dialog found. Use 'start' first.")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
existing.round += 1
|
|
127
|
+
existing.analysis = input.analysis ?? existing.analysis
|
|
128
|
+
existing.openQuestions = input.questions ?? []
|
|
129
|
+
|
|
130
|
+
existing.history.push({
|
|
131
|
+
round: existing.round,
|
|
132
|
+
analysis: existing.analysis,
|
|
133
|
+
questions: existing.openQuestions,
|
|
134
|
+
userResponses: input.userResponses ?? [],
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
await writeState(input.repoRoot, existing)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
artifactPath: existing.artifactPath,
|
|
141
|
+
round: existing.round,
|
|
142
|
+
status: existing.status,
|
|
143
|
+
analysis: existing.analysis,
|
|
144
|
+
openQuestions: existing.openQuestions,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function summarizeDialog(input: BrainstormDialogInput): Promise<BrainstormDialogResult> {
|
|
149
|
+
const existing = await readState(input.repoRoot, input.artifactPath)
|
|
150
|
+
if (!existing) {
|
|
151
|
+
throw new Error("No dialog found. Use 'start' first.")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
existing.status = "complete"
|
|
155
|
+
existing.analysis = input.analysis ?? existing.analysis
|
|
156
|
+
existing.openQuestions = []
|
|
157
|
+
|
|
158
|
+
await writeState(input.repoRoot, existing)
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
artifactPath: existing.artifactPath,
|
|
162
|
+
round: existing.round,
|
|
163
|
+
status: existing.status,
|
|
164
|
+
analysis: existing.analysis,
|
|
165
|
+
openQuestions: [],
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface ParallelSubagentTask {
|
|
2
|
+
agent: string
|
|
3
|
+
task: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ParallelSubagentInput {
|
|
7
|
+
tasks: ParallelSubagentTask[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ParallelResultItem {
|
|
11
|
+
status: "fulfilled" | "rejected"
|
|
12
|
+
value?: string
|
|
13
|
+
reason?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ParallelSubagentResult {
|
|
17
|
+
mode: "parallel"
|
|
18
|
+
outputs: ParallelResultItem[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type SubagentRunner = (prompt: string) => Promise<string>
|
|
22
|
+
|
|
23
|
+
function buildPrompt(agent: string, task: string): string {
|
|
24
|
+
return `/skill:${agent} ${task}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createParallelSubagentTool() {
|
|
28
|
+
return {
|
|
29
|
+
name: "parallel_subagent",
|
|
30
|
+
async execute(input: ParallelSubagentInput, runner: SubagentRunner): Promise<ParallelSubagentResult> {
|
|
31
|
+
if (!input.tasks || input.tasks.length === 0) {
|
|
32
|
+
throw new Error("parallel_subagent requires at least one task")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const promises = input.tasks.map(({ agent, task }) =>
|
|
36
|
+
runner(buildPrompt(agent, task)),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const settled = await Promise.allSettled(promises)
|
|
40
|
+
|
|
41
|
+
const outputs: ParallelResultItem[] = settled.map((result) => {
|
|
42
|
+
if (result.status === "fulfilled") {
|
|
43
|
+
return { status: "fulfilled", value: result.value }
|
|
44
|
+
}
|
|
45
|
+
return { status: "rejected", reason: result.reason?.message || String(result.reason) }
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
mode: "parallel",
|
|
50
|
+
outputs,
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export interface ArtifactInput {
|
|
2
|
+
path: string
|
|
3
|
+
content: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface Pattern {
|
|
7
|
+
keyword: string
|
|
8
|
+
occurrences: number
|
|
9
|
+
sources: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ExtractInput {
|
|
13
|
+
operation: "extract"
|
|
14
|
+
artifacts: ArtifactInput[]
|
|
15
|
+
keywords?: string[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ExtractResult {
|
|
19
|
+
operation: "extract"
|
|
20
|
+
patterns: Pattern[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CategorizeInput {
|
|
24
|
+
operation: "categorize"
|
|
25
|
+
patterns: Pattern[]
|
|
26
|
+
categories: Record<string, string[]>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CategorizeResult {
|
|
30
|
+
operation: "categorize"
|
|
31
|
+
categories: Record<string, Pattern[]>
|
|
32
|
+
uncategorized: Pattern[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type PatternExtractorInput = ExtractInput | CategorizeInput | { operation: string; artifacts?: ArtifactInput[] }
|
|
36
|
+
export type PatternExtractorResult = ExtractResult | CategorizeResult
|
|
37
|
+
|
|
38
|
+
function extractKeywords(content: string): string[] {
|
|
39
|
+
return content
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
42
|
+
.split(/\s+/)
|
|
43
|
+
.filter((w) => w.length > 2)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractWithKeywords(artifacts: ArtifactInput[], keywords: string[]): Pattern[] {
|
|
47
|
+
const patterns: Pattern[] = []
|
|
48
|
+
|
|
49
|
+
for (const keyword of keywords) {
|
|
50
|
+
const sources: string[] = []
|
|
51
|
+
let occurrences = 0
|
|
52
|
+
|
|
53
|
+
for (const artifact of artifacts) {
|
|
54
|
+
const regex = new RegExp(keyword, "gi")
|
|
55
|
+
const matches = artifact.content.match(regex)
|
|
56
|
+
if (matches && matches.length > 0) {
|
|
57
|
+
occurrences += matches.length
|
|
58
|
+
sources.push(artifact.path)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (occurrences > 0) {
|
|
63
|
+
patterns.push({ keyword, occurrences, sources })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return patterns.sort((a, b) => b.occurrences - a.occurrences)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function extractAllKeywords(artifacts: ArtifactInput[]): Pattern[] {
|
|
71
|
+
const wordMap = new Map<string, { count: number; sources: Set<string> }>()
|
|
72
|
+
|
|
73
|
+
for (const artifact of artifacts) {
|
|
74
|
+
const words = extractKeywords(artifact.content)
|
|
75
|
+
const uniqueWords = new Set(words)
|
|
76
|
+
|
|
77
|
+
for (const word of uniqueWords) {
|
|
78
|
+
const existing = wordMap.get(word) ?? { count: 0, sources: new Set<string>() }
|
|
79
|
+
existing.count += words.filter((w) => w === word).length
|
|
80
|
+
existing.sources.add(artifact.path)
|
|
81
|
+
wordMap.set(word, existing)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return Array.from(wordMap.entries())
|
|
86
|
+
.map(([keyword, data]) => ({
|
|
87
|
+
keyword,
|
|
88
|
+
occurrences: data.count,
|
|
89
|
+
sources: Array.from(data.sources),
|
|
90
|
+
}))
|
|
91
|
+
.sort((a, b) => b.occurrences - a.occurrences)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createPatternExtractorTool() {
|
|
95
|
+
return {
|
|
96
|
+
name: "pattern_extractor",
|
|
97
|
+
execute(input: PatternExtractorInput): PatternExtractorResult {
|
|
98
|
+
switch (input.operation) {
|
|
99
|
+
case "extract": {
|
|
100
|
+
const artifacts = (input as ExtractInput).artifacts
|
|
101
|
+
const keywords = (input as ExtractInput).keywords
|
|
102
|
+
const patterns = keywords && keywords.length > 0
|
|
103
|
+
? extractWithKeywords(artifacts, keywords)
|
|
104
|
+
: extractAllKeywords(artifacts)
|
|
105
|
+
return { operation: "extract", patterns }
|
|
106
|
+
}
|
|
107
|
+
case "categorize": {
|
|
108
|
+
const { patterns, categories } = input as CategorizeInput
|
|
109
|
+
const result: Record<string, Pattern[]> = {}
|
|
110
|
+
const uncategorized: Pattern[] = []
|
|
111
|
+
const matched = new Set<string>()
|
|
112
|
+
|
|
113
|
+
for (const [category, catKeywords] of Object.entries(categories)) {
|
|
114
|
+
result[category] = []
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const pattern of patterns) {
|
|
118
|
+
let found = false
|
|
119
|
+
for (const [category, catKeywords] of Object.entries(categories)) {
|
|
120
|
+
if (catKeywords.some((kw) => kw.toLowerCase() === pattern.keyword.toLowerCase())) {
|
|
121
|
+
result[category].push(pattern)
|
|
122
|
+
matched.add(pattern.keyword)
|
|
123
|
+
found = true
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!found) {
|
|
128
|
+
uncategorized.push(pattern)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { operation: "categorize", categories: result, uncategorized }
|
|
133
|
+
}
|
|
134
|
+
default:
|
|
135
|
+
throw new Error(`Unknown operation: ${input.operation}`)
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export interface PlanUnit {
|
|
2
|
+
name: string
|
|
3
|
+
description: string
|
|
4
|
+
files: string[]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface PlanChange {
|
|
8
|
+
action: "add" | "remove" | "modify"
|
|
9
|
+
name: string
|
|
10
|
+
description?: string
|
|
11
|
+
files?: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PlanDiffInput {
|
|
15
|
+
operation: "compare" | "patch"
|
|
16
|
+
existingUnits: PlanUnit[]
|
|
17
|
+
newRequirements?: PlanUnit[]
|
|
18
|
+
changes?: PlanChange[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CompareResult {
|
|
22
|
+
operation: "compare"
|
|
23
|
+
added: PlanUnit[]
|
|
24
|
+
removed: PlanUnit[]
|
|
25
|
+
modified: PlanUnit[]
|
|
26
|
+
unchanged: PlanUnit[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PatchResult {
|
|
30
|
+
operation: "patch"
|
|
31
|
+
units: PlanUnit[]
|
|
32
|
+
appliedChanges: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type PlanDiffResult = CompareResult | PatchResult
|
|
36
|
+
|
|
37
|
+
function unitKey(u: PlanUnit): string {
|
|
38
|
+
return u.name
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function unitsEqual(a: PlanUnit, b: PlanUnit): boolean {
|
|
42
|
+
return (
|
|
43
|
+
a.description === b.description &&
|
|
44
|
+
a.files.length === b.files.length &&
|
|
45
|
+
a.files.every((f, i) => f === b.files[i])
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createPlanDiffTool() {
|
|
50
|
+
return {
|
|
51
|
+
name: "plan_diff",
|
|
52
|
+
execute(input: PlanDiffInput): PlanDiffResult {
|
|
53
|
+
switch (input.operation) {
|
|
54
|
+
case "compare":
|
|
55
|
+
return compare(input.existingUnits, input.newRequirements ?? [])
|
|
56
|
+
case "patch":
|
|
57
|
+
return patch(input.existingUnits, input.changes ?? [])
|
|
58
|
+
default:
|
|
59
|
+
throw new Error(`Unknown operation: ${input.operation}`)
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function compare(existing: PlanUnit[], newReqs: PlanUnit[]): CompareResult {
|
|
66
|
+
const existingMap = new Map(existing.map((u) => [unitKey(u), u]))
|
|
67
|
+
const newMap = new Map(newReqs.map((u) => [unitKey(u), u]))
|
|
68
|
+
|
|
69
|
+
const added: PlanUnit[] = []
|
|
70
|
+
const removed: PlanUnit[] = []
|
|
71
|
+
const modified: PlanUnit[] = []
|
|
72
|
+
const unchanged: PlanUnit[] = []
|
|
73
|
+
|
|
74
|
+
// Find added and modified
|
|
75
|
+
for (const [key, newUnit] of newMap) {
|
|
76
|
+
const existingUnit = existingMap.get(key)
|
|
77
|
+
if (!existingUnit) {
|
|
78
|
+
added.push(newUnit)
|
|
79
|
+
} else if (!unitsEqual(existingUnit, newUnit)) {
|
|
80
|
+
modified.push(newUnit)
|
|
81
|
+
} else {
|
|
82
|
+
unchanged.push(newUnit)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Find removed
|
|
87
|
+
for (const [key, existingUnit] of existingMap) {
|
|
88
|
+
if (!newMap.has(key)) {
|
|
89
|
+
removed.push(existingUnit)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { operation: "compare", added, removed, modified, unchanged }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function patch(existing: PlanUnit[], changes: PlanChange[]): PatchResult {
|
|
97
|
+
const unitMap = new Map(existing.map((u) => [unitKey(u), { ...u }]))
|
|
98
|
+
let appliedChanges = 0
|
|
99
|
+
|
|
100
|
+
for (const change of changes) {
|
|
101
|
+
switch (change.action) {
|
|
102
|
+
case "add":
|
|
103
|
+
unitMap.set(change.name, {
|
|
104
|
+
name: change.name,
|
|
105
|
+
description: change.description ?? "",
|
|
106
|
+
files: change.files ?? [],
|
|
107
|
+
})
|
|
108
|
+
appliedChanges++
|
|
109
|
+
break
|
|
110
|
+
case "remove":
|
|
111
|
+
if (unitMap.has(change.name)) {
|
|
112
|
+
unitMap.delete(change.name)
|
|
113
|
+
appliedChanges++
|
|
114
|
+
}
|
|
115
|
+
break
|
|
116
|
+
case "modify": {
|
|
117
|
+
const existing = unitMap.get(change.name)
|
|
118
|
+
if (existing) {
|
|
119
|
+
unitMap.set(change.name, {
|
|
120
|
+
...existing,
|
|
121
|
+
description: change.description ?? existing.description,
|
|
122
|
+
files: change.files ?? existing.files,
|
|
123
|
+
})
|
|
124
|
+
appliedChanges++
|
|
125
|
+
}
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
operation: "patch",
|
|
133
|
+
units: Array.from(unitMap.values()),
|
|
134
|
+
appliedChanges,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export interface ReviewRouterInput {
|
|
2
|
+
filesChanged: string[]
|
|
3
|
+
insertions: number
|
|
4
|
+
deletions: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ReviewerRecommendation {
|
|
8
|
+
name: string
|
|
9
|
+
reason: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ReviewRouterResult {
|
|
13
|
+
reviewers: ReviewerRecommendation[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ReviewerRule {
|
|
17
|
+
name: string
|
|
18
|
+
reason: string
|
|
19
|
+
condition: (input: ReviewRouterInput) => boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const BASE_REVIEWERS: ReviewerRule[] = [
|
|
23
|
+
{
|
|
24
|
+
name: "correctness-reviewer",
|
|
25
|
+
reason: "Always review for logical correctness and intended behavior.",
|
|
26
|
+
condition: () => true,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "testing-reviewer",
|
|
30
|
+
reason: "Always review test coverage and test quality.",
|
|
31
|
+
condition: () => true,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "maintainability-reviewer",
|
|
35
|
+
reason: "Always review code clarity, naming, and structure.",
|
|
36
|
+
condition: () => true,
|
|
37
|
+
},
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
const CONDITIONAL_REVIEWERS: ReviewerRule[] = [
|
|
41
|
+
{
|
|
42
|
+
name: "security-reviewer",
|
|
43
|
+
reason: "Auth, permissions, or user input files were changed.",
|
|
44
|
+
condition: (input) =>
|
|
45
|
+
input.filesChanged.some((f) =>
|
|
46
|
+
/auth|permission|login|password|token|session|credential|crypto/i.test(f),
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "performance-reviewer",
|
|
51
|
+
reason: "Data, query, or cache-related files were changed.",
|
|
52
|
+
condition: (input) =>
|
|
53
|
+
input.filesChanged.some((f) =>
|
|
54
|
+
/query|cache|database|db\/|sql|perf|benchmark|stream|batch/i.test(f),
|
|
55
|
+
),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "integration-reviewer",
|
|
59
|
+
reason: "Configuration, CI/CD, or package files were changed.",
|
|
60
|
+
condition: (input) =>
|
|
61
|
+
input.filesChanged.some((f) =>
|
|
62
|
+
/workflows|docker|compose|package\.json|tsconfig|\.env|config/i.test(f),
|
|
63
|
+
),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "thoroughness-reviewer",
|
|
67
|
+
reason: "Large diff with many changes across multiple files.",
|
|
68
|
+
condition: (input) =>
|
|
69
|
+
input.filesChanged.length >= 5 ||
|
|
70
|
+
input.insertions + input.deletions >= 300,
|
|
71
|
+
},
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
export function createReviewRouterTool() {
|
|
75
|
+
return {
|
|
76
|
+
name: "review_router",
|
|
77
|
+
async execute(input: ReviewRouterInput): Promise<ReviewRouterResult> {
|
|
78
|
+
const reviewers: ReviewerRecommendation[] = []
|
|
79
|
+
|
|
80
|
+
for (const rule of BASE_REVIEWERS) {
|
|
81
|
+
if (rule.condition(input)) {
|
|
82
|
+
reviewers.push({ name: rule.name, reason: rule.reason })
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const rule of CONDITIONAL_REVIEWERS) {
|
|
87
|
+
if (rule.condition(input)) {
|
|
88
|
+
reviewers.push({ name: rule.name, reason: rule.reason })
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { reviewers }
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|