@almightygpt/core 0.2.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/dist/adapters/claude.d.ts +31 -0
- package/dist/adapters/claude.d.ts.map +1 -0
- package/dist/adapters/claude.js +90 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/gemini.d.ts +42 -0
- package/dist/adapters/gemini.d.ts.map +1 -0
- package/dist/adapters/gemini.js +133 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/index.d.ts +16 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +15 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/mock.d.ts +23 -0
- package/dist/adapters/mock.d.ts.map +1 -0
- package/dist/adapters/mock.js +107 -0
- package/dist/adapters/mock.js.map +1 -0
- package/dist/adapters/openai.d.ts +38 -0
- package/dist/adapters/openai.d.ts.map +1 -0
- package/dist/adapters/openai.js +105 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/adapters/types.d.ts +65 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +26 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/config/load.d.ts +15 -0
- package/dist/config/load.d.ts.map +1 -0
- package/dist/config/load.js +46 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/schema.d.ts +260 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +58 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/context/manifest.d.ts +58 -0
- package/dist/context/manifest.d.ts.map +1 -0
- package/dist/context/manifest.js +49 -0
- package/dist/context/manifest.js.map +1 -0
- package/dist/context/redact.d.ts +26 -0
- package/dist/context/redact.d.ts.map +1 -0
- package/dist/context/redact.js +67 -0
- package/dist/context/redact.js.map +1 -0
- package/dist/git/status.d.ts +48 -0
- package/dist/git/status.d.ts.map +1 -0
- package/dist/git/status.js +79 -0
- package/dist/git/status.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/review/budget.d.ts +46 -0
- package/dist/review/budget.d.ts.map +1 -0
- package/dist/review/budget.js +83 -0
- package/dist/review/budget.js.map +1 -0
- package/dist/review/diff.d.ts +21 -0
- package/dist/review/diff.d.ts.map +1 -0
- package/dist/review/diff.js +55 -0
- package/dist/review/diff.js.map +1 -0
- package/dist/review/events.d.ts +76 -0
- package/dist/review/events.d.ts.map +1 -0
- package/dist/review/events.js +13 -0
- package/dist/review/events.js.map +1 -0
- package/dist/review/memory.d.ts +23 -0
- package/dist/review/memory.d.ts.map +1 -0
- package/dist/review/memory.js +42 -0
- package/dist/review/memory.js.map +1 -0
- package/dist/review/prompts.d.ts +34 -0
- package/dist/review/prompts.d.ts.map +1 -0
- package/dist/review/prompts.js +174 -0
- package/dist/review/prompts.js.map +1 -0
- package/dist/review/run-diff-review.d.ts +52 -0
- package/dist/review/run-diff-review.d.ts.map +1 -0
- package/dist/review/run-diff-review.js +258 -0
- package/dist/review/run-diff-review.js.map +1 -0
- package/dist/review/run-worker-reviewer.d.ts +72 -0
- package/dist/review/run-worker-reviewer.d.ts.map +1 -0
- package/dist/review/run-worker-reviewer.js +407 -0
- package/dist/review/run-worker-reviewer.js.map +1 -0
- package/dist/review/write.d.ts +44 -0
- package/dist/review/write.d.ts.map +1 -0
- package/dist/review/write.js +152 -0
- package/dist/review/write.js.map +1 -0
- package/dist/runs/decide.d.ts +45 -0
- package/dist/runs/decide.d.ts.map +1 -0
- package/dist/runs/decide.js +93 -0
- package/dist/runs/decide.js.map +1 -0
- package/dist/runs/folder.d.ts +42 -0
- package/dist/runs/folder.d.ts.map +1 -0
- package/dist/runs/folder.js +82 -0
- package/dist/runs/folder.js.map +1 -0
- package/dist/runs/list.d.ts +58 -0
- package/dist/runs/list.d.ts.map +1 -0
- package/dist/runs/list.js +117 -0
- package/dist/runs/list.js.map +1 -0
- package/dist/runs/types.d.ts +96 -0
- package/dist/runs/types.d.ts.map +1 -0
- package/dist/runs/types.js +13 -0
- package/dist/runs/types.js.map +1 -0
- package/dist/templates/install.d.ts +49 -0
- package/dist/templates/install.d.ts.map +1 -0
- package/dist/templates/install.js +154 -0
- package/dist/templates/install.js.map +1 -0
- package/package.json +34 -0
- package/src/adapters/claude.ts +133 -0
- package/src/adapters/gemini.ts +183 -0
- package/src/adapters/index.ts +21 -0
- package/src/adapters/mock.ts +125 -0
- package/src/adapters/openai.ts +150 -0
- package/src/adapters/types.ts +73 -0
- package/src/config/load.ts +61 -0
- package/src/config/schema.ts +64 -0
- package/src/context/manifest.ts +94 -0
- package/src/context/redact.ts +93 -0
- package/src/git/status.ts +108 -0
- package/src/index.ts +127 -0
- package/src/review/budget.ts +116 -0
- package/src/review/diff.ts +85 -0
- package/src/review/events.ts +86 -0
- package/src/review/memory.ts +57 -0
- package/src/review/prompts.ts +208 -0
- package/src/review/run-diff-review.ts +353 -0
- package/src/review/run-worker-reviewer.ts +528 -0
- package/src/review/write.ts +208 -0
- package/src/runs/decide.ts +153 -0
- package/src/runs/folder.ts +137 -0
- package/src/runs/list.ts +152 -0
- package/src/runs/types.ts +98 -0
- package/src/templates/install.ts +198 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load and assemble the agent memory + project context that gets fed to an
|
|
3
|
+
* adapter as its system prompt.
|
|
4
|
+
*
|
|
5
|
+
* For a Reviewer call, the assembled prompt is:
|
|
6
|
+
* 1. AGENTS.md (shared canonical context)
|
|
7
|
+
* 2. The agent's memory file (e.g. CODEX_AGENT.md)
|
|
8
|
+
* 3. .almightygpt/rules.md (project-wide invariants)
|
|
9
|
+
*
|
|
10
|
+
* Files are concatenated with clear section dividers so the model can tell
|
|
11
|
+
* which guidance comes from where.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFile } from "node:fs/promises";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
export interface MemoryAssembly {
|
|
18
|
+
text: string;
|
|
19
|
+
sources: { path: string; bytes: number }[];
|
|
20
|
+
/** Paths that were expected but missing. Surfaces to the user as a warning. */
|
|
21
|
+
missing: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SECTION_DIVIDER =
|
|
25
|
+
"\n\n---\n# Section: %SOURCE%\n# (loaded by AlmightyGPT)\n---\n\n";
|
|
26
|
+
|
|
27
|
+
export async function assembleMemory(
|
|
28
|
+
repoRoot: string,
|
|
29
|
+
memoryFile: string,
|
|
30
|
+
): Promise<MemoryAssembly> {
|
|
31
|
+
const candidates = [
|
|
32
|
+
"AGENTS.md",
|
|
33
|
+
memoryFile,
|
|
34
|
+
join(".almightygpt", "rules.md"),
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const parts: string[] = [];
|
|
38
|
+
const sources: { path: string; bytes: number }[] = [];
|
|
39
|
+
const missing: string[] = [];
|
|
40
|
+
|
|
41
|
+
for (const rel of candidates) {
|
|
42
|
+
const abs = join(repoRoot, rel);
|
|
43
|
+
try {
|
|
44
|
+
const content = await readFile(abs, "utf8");
|
|
45
|
+
parts.push(SECTION_DIVIDER.replace("%SOURCE%", rel) + content.trim());
|
|
46
|
+
sources.push({ path: rel, bytes: content.length });
|
|
47
|
+
} catch {
|
|
48
|
+
missing.push(rel);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
text: parts.join("\n").trim(),
|
|
54
|
+
sources,
|
|
55
|
+
missing,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt assembly for review runs.
|
|
3
|
+
*
|
|
4
|
+
* The Reviewer's job is constrained by its memory file (CODEX_AGENT.md) and
|
|
5
|
+
* the project rules (.almightygpt/rules.md), which are assembled into the
|
|
6
|
+
* system prompt by review/memory.ts. The user message here supplies the
|
|
7
|
+
* concrete task.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface ReviewerUserMessageOptions {
|
|
11
|
+
topic: string;
|
|
12
|
+
diff: string;
|
|
13
|
+
files: string[];
|
|
14
|
+
/** Optional repo-relative path the user pointed at (e.g. for path reviews). */
|
|
15
|
+
scope?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface WorkerUserMessageOptions {
|
|
19
|
+
topic: string;
|
|
20
|
+
diff: string;
|
|
21
|
+
files: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ReviewerOfWorkerUserMessageOptions {
|
|
25
|
+
topic: string;
|
|
26
|
+
diff: string;
|
|
27
|
+
files: string[];
|
|
28
|
+
workerOutput: string;
|
|
29
|
+
workerAgent: string;
|
|
30
|
+
workerProvider: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildReviewerSystemFraming(): string {
|
|
34
|
+
// Brief framing that goes in front of the assembled memory text. The
|
|
35
|
+
// assembled memory contains the anti-sycophancy rules and the format spec.
|
|
36
|
+
// We use this top-level framing to (a) anchor the model on THIS diff,
|
|
37
|
+
// not the whole repo, and (b) clarify which sections the orchestrator
|
|
38
|
+
// owns vs which the model must produce.
|
|
39
|
+
return [
|
|
40
|
+
"You are the Reviewer AI in an AlmightyGPT cross-review run.",
|
|
41
|
+
"",
|
|
42
|
+
"WHAT YOU ARE REVIEWING: only the diff supplied below.",
|
|
43
|
+
"Do not audit unchanged code, unchanged docs, or the project structure",
|
|
44
|
+
"as a whole. The assembled project memory below is BACKGROUND CONTEXT",
|
|
45
|
+
"for interpreting the diff — it is not the review target. If a finding",
|
|
46
|
+
"could apply equally to any commit in the repo, it is too broad for",
|
|
47
|
+
"this review.",
|
|
48
|
+
"",
|
|
49
|
+
"WHAT THE ORCHESTRATOR OWNS (do NOT include these in your response):",
|
|
50
|
+
" - The H1 title (the orchestrator prepends `# Review: <topic>`).",
|
|
51
|
+
" - The header block with model / tokens / cost / latency.",
|
|
52
|
+
" - The `## Cost and Latency` section.",
|
|
53
|
+
" - The `## Appendix: Raw Outputs` section.",
|
|
54
|
+
"",
|
|
55
|
+
"WHAT YOU MUST PRODUCE — start your response with `## Decision Required`",
|
|
56
|
+
"and emit only these sections in this order:",
|
|
57
|
+
" ## Decision Required",
|
|
58
|
+
" ## Highest-Risk Findings",
|
|
59
|
+
" ## Concrete Weaknesses",
|
|
60
|
+
" ## Test Plan",
|
|
61
|
+
" ## Human Decision",
|
|
62
|
+
"",
|
|
63
|
+
"Anti-sycophancy is non-negotiable: list at least three concrete",
|
|
64
|
+
"weaknesses with file:line references taken from the diff hunks (the",
|
|
65
|
+
"`@@ -X,Y +N,M @@` markers tell you starting line numbers — use real",
|
|
66
|
+
"ones, do not invent). If you cannot find three real weaknesses in",
|
|
67
|
+
"this diff, say so explicitly; your output will be flagged shallow.",
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildWorkerSystemFraming(): string {
|
|
72
|
+
return [
|
|
73
|
+
"You are the Worker AI in an AlmightyGPT cross-review run.",
|
|
74
|
+
"Read the assembled project memory below, then read the diff and produce",
|
|
75
|
+
"a thorough plan that the Reviewer AI will critique. Your job is to be",
|
|
76
|
+
"honest about uncertainty and explicit about risks. The Reviewer will",
|
|
77
|
+
"find what you missed — make their job easier by being precise about",
|
|
78
|
+
"what you considered and what you did not.",
|
|
79
|
+
].join(" ");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function buildWorkerUserMessage(opts: WorkerUserMessageOptions): string {
|
|
83
|
+
const filesList = opts.files.length
|
|
84
|
+
? opts.files.map((f) => `- ${f}`).join("\n")
|
|
85
|
+
: "(no files reported)";
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
`# Worker Task`,
|
|
89
|
+
"",
|
|
90
|
+
`**Topic:** \`${opts.topic}\``,
|
|
91
|
+
"",
|
|
92
|
+
`## Files in this diff`,
|
|
93
|
+
"",
|
|
94
|
+
filesList,
|
|
95
|
+
"",
|
|
96
|
+
`## Diff`,
|
|
97
|
+
"",
|
|
98
|
+
"```diff",
|
|
99
|
+
opts.diff.trim() || "(empty diff)",
|
|
100
|
+
"```",
|
|
101
|
+
"",
|
|
102
|
+
`## Your Instructions`,
|
|
103
|
+
"",
|
|
104
|
+
"Produce a markdown response with these sections in order:",
|
|
105
|
+
"",
|
|
106
|
+
"1. `## Change Summary` — one paragraph: what does this diff do, and why?",
|
|
107
|
+
"2. `## Affected Modules / Surfaces` — bulleted list, each with one-line impact.",
|
|
108
|
+
"3. `## Risks` — bulleted list. Each risk: what could go wrong, where, and how likely. Be specific.",
|
|
109
|
+
"4. `## Test Plan` — what to verify before merging. Each item: scope + acceptance criterion.",
|
|
110
|
+
"5. `## Open Questions` — things you are not sure about. List them honestly. The Reviewer needs to know where to look hardest.",
|
|
111
|
+
"",
|
|
112
|
+
"Output markdown only. Do not wrap the whole response in a code block.",
|
|
113
|
+
"Do not produce a Reviewer-style critique — that is the Reviewer's job in",
|
|
114
|
+
"the next stage.",
|
|
115
|
+
].join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function buildReviewerOfWorkerUserMessage(
|
|
119
|
+
opts: ReviewerOfWorkerUserMessageOptions,
|
|
120
|
+
): string {
|
|
121
|
+
const filesList = opts.files.length
|
|
122
|
+
? opts.files.map((f) => `- ${f}`).join("\n")
|
|
123
|
+
: "(no files reported)";
|
|
124
|
+
|
|
125
|
+
return [
|
|
126
|
+
`# Critique This Worker Output`,
|
|
127
|
+
"",
|
|
128
|
+
`**Topic:** \`${opts.topic}\``,
|
|
129
|
+
`**Worker:** \`${opts.workerAgent}\` (${opts.workerProvider})`,
|
|
130
|
+
"",
|
|
131
|
+
`## Files in this diff`,
|
|
132
|
+
"",
|
|
133
|
+
filesList,
|
|
134
|
+
"",
|
|
135
|
+
`## Diff (what the Worker analyzed — this is the change in question)`,
|
|
136
|
+
"",
|
|
137
|
+
"```diff",
|
|
138
|
+
opts.diff.trim() || "(empty diff)",
|
|
139
|
+
"```",
|
|
140
|
+
"",
|
|
141
|
+
`## Worker Output (this is what you are critiquing)`,
|
|
142
|
+
"",
|
|
143
|
+
opts.workerOutput.trim(),
|
|
144
|
+
"",
|
|
145
|
+
`## Reminders`,
|
|
146
|
+
"",
|
|
147
|
+
"- Start your response with `## Decision Required` — the orchestrator",
|
|
148
|
+
" owns the H1 title and the cost header.",
|
|
149
|
+
"- Findings must point at the diff or at specific claims/omissions in",
|
|
150
|
+
" the Worker's output. Do not audit unchanged code or unchanged docs.",
|
|
151
|
+
"- File:line references must come from the diff's `@@ -X,Y +N,M @@`",
|
|
152
|
+
" hunk markers. Do not invent line numbers.",
|
|
153
|
+
"- Required sections in order:",
|
|
154
|
+
" - `## Decision Required`",
|
|
155
|
+
" - `## Highest-Risk Findings`",
|
|
156
|
+
" - `## Concrete Weaknesses` (at least 3 with file:line refs; missing",
|
|
157
|
+
" Worker considerations count if specific)",
|
|
158
|
+
" - `## Worker Plan Summary` (condensed)",
|
|
159
|
+
" - `## Test Plan`",
|
|
160
|
+
" - `## Human Decision` (leave body blank)",
|
|
161
|
+
"- Do NOT include `## Cost and Latency` or `## Appendix: Raw Outputs`",
|
|
162
|
+
" — the orchestrator emits those after you.",
|
|
163
|
+
"- The Worker is not the user; the Worker is what you are critiquing.",
|
|
164
|
+
" Disagree where they are wrong, do not soften.",
|
|
165
|
+
"- Markdown only. No code-block wrapper around the whole response.",
|
|
166
|
+
].join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function buildReviewerUserMessage(
|
|
170
|
+
opts: ReviewerUserMessageOptions,
|
|
171
|
+
): string {
|
|
172
|
+
const filesList = opts.files.length
|
|
173
|
+
? opts.files.map((f) => `- ${f}`).join("\n")
|
|
174
|
+
: "(no files reported)";
|
|
175
|
+
|
|
176
|
+
return [
|
|
177
|
+
`# Review This Diff`,
|
|
178
|
+
"",
|
|
179
|
+
`**Topic:** \`${opts.topic}\``,
|
|
180
|
+
opts.scope ? `**Scope:** \`${opts.scope}\`` : "",
|
|
181
|
+
"",
|
|
182
|
+
`## Files in this diff`,
|
|
183
|
+
"",
|
|
184
|
+
filesList,
|
|
185
|
+
"",
|
|
186
|
+
`## Diff (this is the ONLY thing you are reviewing)`,
|
|
187
|
+
"",
|
|
188
|
+
"```diff",
|
|
189
|
+
opts.diff.trim() || "(empty diff)",
|
|
190
|
+
"```",
|
|
191
|
+
"",
|
|
192
|
+
`## Reminders`,
|
|
193
|
+
"",
|
|
194
|
+
"- Start your response with `## Decision Required` — the orchestrator",
|
|
195
|
+
" owns the H1 title and the cost header.",
|
|
196
|
+
"- Findings must point at the diff above, not at unchanged code or",
|
|
197
|
+
" unchanged docs.",
|
|
198
|
+
"- File:line references must come from the diff's `@@ -X,Y +N,M @@`",
|
|
199
|
+
" hunk markers. Do not invent line numbers.",
|
|
200
|
+
"- If this diff is small or routine, say so plainly in Decision Required.",
|
|
201
|
+
" Honest small reviews beat padded long ones.",
|
|
202
|
+
"- Do NOT include `## Cost and Latency` or `## Appendix: Raw Outputs`",
|
|
203
|
+
" sections — the orchestrator emits those after you.",
|
|
204
|
+
"- Markdown only. No code-block wrapper around the whole response.",
|
|
205
|
+
]
|
|
206
|
+
.filter(Boolean)
|
|
207
|
+
.join("\n");
|
|
208
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrate a diff review (now with dual artifact output, redaction, and
|
|
3
|
+
* budget enforcement).
|
|
4
|
+
*
|
|
5
|
+
* Pipeline:
|
|
6
|
+
* 1. Load config + create run folder.
|
|
7
|
+
* 2. Resolve the reviewer adapter.
|
|
8
|
+
* 3. Collect the git diff.
|
|
9
|
+
* 4. Redact secrets from the diff (config.security.redactSecrets).
|
|
10
|
+
* 5. Build context manifest, write input.md + context-manifest.json.
|
|
11
|
+
* 6. Assemble the reviewer's system prompt (AGENTS.md + memory + rules).
|
|
12
|
+
* 7. Pre-flight budget check.
|
|
13
|
+
* 8. Call the adapter.
|
|
14
|
+
* 9. Record adapter call against the budget (post-call check).
|
|
15
|
+
* 10. Detect shallow review.
|
|
16
|
+
* 11. Write outputs/reviewer.md (machine) and the human review file.
|
|
17
|
+
* 12. Write run.json with totals + status.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { type Adapter, AdapterError } from "../adapters/types.js";
|
|
21
|
+
import { MockAdapter } from "../adapters/mock.js";
|
|
22
|
+
import { OpenAIAdapter } from "../adapters/openai.js";
|
|
23
|
+
import { ClaudeAdapter } from "../adapters/claude.js";
|
|
24
|
+
import { GeminiAdapter } from "../adapters/gemini.js";
|
|
25
|
+
import { loadConfig } from "../config/load.js";
|
|
26
|
+
import { redactSecrets } from "../context/redact.js";
|
|
27
|
+
import {
|
|
28
|
+
buildContextManifest,
|
|
29
|
+
writeContextManifest,
|
|
30
|
+
} from "../context/manifest.js";
|
|
31
|
+
import {
|
|
32
|
+
collectGitContext,
|
|
33
|
+
createRunFolder,
|
|
34
|
+
writeAgentOutput,
|
|
35
|
+
writeRunInput,
|
|
36
|
+
writeRunMetadata,
|
|
37
|
+
} from "../runs/folder.js";
|
|
38
|
+
import type { RunMetadata } from "../runs/types.js";
|
|
39
|
+
import { collectGitDiff } from "./diff.js";
|
|
40
|
+
import { assembleMemory } from "./memory.js";
|
|
41
|
+
import {
|
|
42
|
+
buildReviewerSystemFraming,
|
|
43
|
+
buildReviewerUserMessage,
|
|
44
|
+
} from "./prompts.js";
|
|
45
|
+
import { writeHumanReviewFile } from "./write.js";
|
|
46
|
+
import { BudgetTracker, BudgetExceededError } from "./budget.js";
|
|
47
|
+
|
|
48
|
+
export interface DiffReviewOptions {
|
|
49
|
+
repoRoot: string;
|
|
50
|
+
topic: string;
|
|
51
|
+
/** Override the reviewer agent name from config defaults. */
|
|
52
|
+
reviewer?: string;
|
|
53
|
+
/** Optional git range like "HEAD~1..HEAD". */
|
|
54
|
+
range?: string;
|
|
55
|
+
/** Bypass the git status safety check. */
|
|
56
|
+
force?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DiffReviewResult {
|
|
60
|
+
reviewPath: string;
|
|
61
|
+
reviewBytes: number;
|
|
62
|
+
reviewer: string;
|
|
63
|
+
provider: string;
|
|
64
|
+
modelUsed: string;
|
|
65
|
+
tokensIn: number;
|
|
66
|
+
tokensOut: number;
|
|
67
|
+
costUsd: number;
|
|
68
|
+
latencyMs: number;
|
|
69
|
+
diffEmpty: boolean;
|
|
70
|
+
filesReviewed: string[];
|
|
71
|
+
memorySources: { path: string; bytes: number }[];
|
|
72
|
+
memoryMissing: string[];
|
|
73
|
+
runId: string;
|
|
74
|
+
runFolder: string;
|
|
75
|
+
redactionsTotal: number;
|
|
76
|
+
shallowWarning?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function runDiffReview(
|
|
80
|
+
opts: DiffReviewOptions,
|
|
81
|
+
): Promise<DiffReviewResult> {
|
|
82
|
+
const config = await loadConfig(opts.repoRoot);
|
|
83
|
+
|
|
84
|
+
const reviewerName = opts.reviewer ?? config.defaults.reviewer;
|
|
85
|
+
if (!reviewerName) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
"No reviewer specified. Pass --reviewer <name> or set defaults.reviewer in .almightygpt/config.yaml.",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const agentConfig = config.agents[reviewerName];
|
|
92
|
+
if (!agentConfig) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Reviewer "${reviewerName}" not found in .almightygpt/config.yaml agents map.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (!agentConfig.enabled) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Reviewer "${reviewerName}" is disabled in .almightygpt/config.yaml.`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const adapter = makeAdapter(reviewerName, agentConfig.provider);
|
|
104
|
+
if (!(await adapter.isAvailable())) {
|
|
105
|
+
throw new AdapterError(
|
|
106
|
+
`Adapter "${reviewerName}" (${agentConfig.provider}) is not available. ` +
|
|
107
|
+
envHintForProvider(agentConfig.provider),
|
|
108
|
+
reviewerName,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Create run folder up front so partial failures still leave an artifact.
|
|
113
|
+
const runFolder = await createRunFolder({
|
|
114
|
+
repoRoot: opts.repoRoot,
|
|
115
|
+
runsDir: config.runsDir,
|
|
116
|
+
topic: opts.topic,
|
|
117
|
+
type: "review-diff",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const git = await collectGitContext(opts.repoRoot);
|
|
121
|
+
|
|
122
|
+
const baseMeta: RunMetadata = {
|
|
123
|
+
id: runFolder.id,
|
|
124
|
+
type: "review-diff",
|
|
125
|
+
createdAt: new Date().toISOString(),
|
|
126
|
+
workspacePath: opts.repoRoot,
|
|
127
|
+
topic: opts.topic,
|
|
128
|
+
git,
|
|
129
|
+
input: opts.range
|
|
130
|
+
? { source: "diff-range", range: opts.range }
|
|
131
|
+
: { source: "diff" },
|
|
132
|
+
agents: Object.entries(config.agents).map(([name, a]) => ({
|
|
133
|
+
name,
|
|
134
|
+
role: a.role,
|
|
135
|
+
provider: a.provider,
|
|
136
|
+
enabled: a.enabled,
|
|
137
|
+
})),
|
|
138
|
+
adapterVersions: [{ adapter: adapter.name, version: "0.0.0" }],
|
|
139
|
+
status: "running",
|
|
140
|
+
metrics: [],
|
|
141
|
+
totals: { tokensIn: 0, tokensOut: 0, costUsd: 0, latencyMs: 0 },
|
|
142
|
+
budget: config.budget,
|
|
143
|
+
};
|
|
144
|
+
// Persist the running state so external tools / dashboards can poll.
|
|
145
|
+
await writeRunMetadata(runFolder.absPath, baseMeta);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const diffArgs: { range?: string } = {};
|
|
149
|
+
if (opts.range) diffArgs.range = opts.range;
|
|
150
|
+
const diffResult = await collectGitDiff(opts.repoRoot, diffArgs);
|
|
151
|
+
if (diffResult.empty && !opts.range) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
"No uncommitted changes to review. Stage some changes, pick a range with " +
|
|
154
|
+
"--range, or pass --topic to a non-empty diff.",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const redaction = config.security.redactSecrets
|
|
159
|
+
? redactSecrets(diffResult.diff)
|
|
160
|
+
: { text: diffResult.diff, redactions: [], totalCount: 0 };
|
|
161
|
+
|
|
162
|
+
const manifest = buildContextManifest({
|
|
163
|
+
inputSource: opts.range ? "diff-range" : "diff",
|
|
164
|
+
filesIncluded: diffResult.files.map((p) => ({ path: p, bytes: 0 })),
|
|
165
|
+
filesSkipped: [],
|
|
166
|
+
diffText: redaction.text,
|
|
167
|
+
redaction: {
|
|
168
|
+
enabled: config.security.redactSecrets,
|
|
169
|
+
totalCount: redaction.totalCount,
|
|
170
|
+
byKind: redaction.redactions,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
const manifestRelPath = await writeContextManifest(
|
|
174
|
+
runFolder.absPath,
|
|
175
|
+
manifest,
|
|
176
|
+
);
|
|
177
|
+
await writeRunInput(runFolder.absPath, redaction.text);
|
|
178
|
+
|
|
179
|
+
const memory = await assembleMemory(opts.repoRoot, agentConfig.memoryFile);
|
|
180
|
+
const systemPrompt =
|
|
181
|
+
buildReviewerSystemFraming() + "\n\n" + memory.text;
|
|
182
|
+
const userMessage = buildReviewerUserMessage({
|
|
183
|
+
topic: opts.topic,
|
|
184
|
+
diff: redaction.text,
|
|
185
|
+
files: diffResult.files,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const budget = new BudgetTracker(config.budget);
|
|
189
|
+
const estimatedTokensIn = Math.ceil(
|
|
190
|
+
(systemPrompt.length + userMessage.length) / 4,
|
|
191
|
+
);
|
|
192
|
+
budget.preflightCheck({
|
|
193
|
+
model: "gpt-4o", // estimator only; adapter resolves real model
|
|
194
|
+
estimatedTokensIn,
|
|
195
|
+
maxOutputTokens: 4096,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const adapterOut = await adapter.execute({
|
|
199
|
+
role: "reviewer",
|
|
200
|
+
systemPrompt,
|
|
201
|
+
userMessage,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
budget.record({
|
|
205
|
+
tokensIn: adapterOut.tokensIn,
|
|
206
|
+
tokensOut: adapterOut.tokensOut,
|
|
207
|
+
costUsd: adapterOut.costUsd,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await writeAgentOutput(runFolder.absPath, "reviewer", adapterOut.content);
|
|
211
|
+
|
|
212
|
+
const shallowWarning = detectShallowReview(
|
|
213
|
+
adapterOut.content,
|
|
214
|
+
config.review.requireConcreteWeaknesses,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const writeOpts: Parameters<typeof writeHumanReviewFile>[0] = {
|
|
218
|
+
repoRoot: opts.repoRoot,
|
|
219
|
+
reviewsDir: config.reviewsDir,
|
|
220
|
+
topic: opts.topic,
|
|
221
|
+
reviewerName,
|
|
222
|
+
reviewerProvider: adapter.provider,
|
|
223
|
+
modelUsed: adapterOut.modelUsed,
|
|
224
|
+
body: adapterOut.content,
|
|
225
|
+
metrics: {
|
|
226
|
+
tokensIn: adapterOut.tokensIn,
|
|
227
|
+
tokensOut: adapterOut.tokensOut,
|
|
228
|
+
costUsd: adapterOut.costUsd,
|
|
229
|
+
latencyMs: adapterOut.latencyMs,
|
|
230
|
+
},
|
|
231
|
+
runFolder: runFolder.relPath,
|
|
232
|
+
};
|
|
233
|
+
if (shallowWarning) writeOpts.shallowWarning = shallowWarning;
|
|
234
|
+
if (opts.force) writeOpts.force = opts.force;
|
|
235
|
+
const written = await writeHumanReviewFile(writeOpts);
|
|
236
|
+
|
|
237
|
+
const finalMeta: RunMetadata = {
|
|
238
|
+
...baseMeta,
|
|
239
|
+
finishedAt: new Date().toISOString(),
|
|
240
|
+
status: "completed",
|
|
241
|
+
contextManifestPath: manifestRelPath,
|
|
242
|
+
reviewPath: written.path,
|
|
243
|
+
metrics: [
|
|
244
|
+
{
|
|
245
|
+
agent: reviewerName,
|
|
246
|
+
role: "reviewer",
|
|
247
|
+
provider: adapter.provider,
|
|
248
|
+
model: adapterOut.modelUsed,
|
|
249
|
+
tokensIn: adapterOut.tokensIn,
|
|
250
|
+
tokensOut: adapterOut.tokensOut,
|
|
251
|
+
costUsd: adapterOut.costUsd,
|
|
252
|
+
latencyMs: adapterOut.latencyMs,
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
totals: {
|
|
256
|
+
tokensIn: adapterOut.tokensIn,
|
|
257
|
+
tokensOut: adapterOut.tokensOut,
|
|
258
|
+
costUsd: adapterOut.costUsd,
|
|
259
|
+
latencyMs: adapterOut.latencyMs,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
await writeRunMetadata(runFolder.absPath, finalMeta);
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
reviewPath: written.path,
|
|
266
|
+
reviewBytes: written.bytes,
|
|
267
|
+
reviewer: reviewerName,
|
|
268
|
+
provider: adapter.provider,
|
|
269
|
+
modelUsed: adapterOut.modelUsed,
|
|
270
|
+
tokensIn: adapterOut.tokensIn,
|
|
271
|
+
tokensOut: adapterOut.tokensOut,
|
|
272
|
+
costUsd: adapterOut.costUsd,
|
|
273
|
+
latencyMs: adapterOut.latencyMs,
|
|
274
|
+
diffEmpty: diffResult.empty,
|
|
275
|
+
filesReviewed: diffResult.files,
|
|
276
|
+
memorySources: memory.sources,
|
|
277
|
+
memoryMissing: memory.missing,
|
|
278
|
+
runId: runFolder.id,
|
|
279
|
+
runFolder: runFolder.relPath,
|
|
280
|
+
redactionsTotal: redaction.totalCount,
|
|
281
|
+
...(shallowWarning ? { shallowWarning } : {}),
|
|
282
|
+
};
|
|
283
|
+
} catch (err) {
|
|
284
|
+
const failedMeta: RunMetadata = {
|
|
285
|
+
...baseMeta,
|
|
286
|
+
finishedAt: new Date().toISOString(),
|
|
287
|
+
status: err instanceof BudgetExceededError ? "aborted_budget" : "failed",
|
|
288
|
+
error: {
|
|
289
|
+
name: err instanceof Error ? err.name : "Error",
|
|
290
|
+
message: err instanceof Error ? err.message : String(err),
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
await writeRunMetadata(runFolder.absPath, failedMeta);
|
|
294
|
+
throw err;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function makeAdapter(name: string, provider: string): Adapter {
|
|
299
|
+
switch (provider) {
|
|
300
|
+
case "openai":
|
|
301
|
+
return new OpenAIAdapter(name);
|
|
302
|
+
case "anthropic":
|
|
303
|
+
return new ClaudeAdapter(name);
|
|
304
|
+
case "google":
|
|
305
|
+
return new GeminiAdapter(name);
|
|
306
|
+
case "mock":
|
|
307
|
+
return new MockAdapter();
|
|
308
|
+
default:
|
|
309
|
+
throw new Error(
|
|
310
|
+
`Provider "${provider}" not supported. ` +
|
|
311
|
+
`Use "openai", "anthropic", "google", or "mock".`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function envHintForProvider(provider: string): string {
|
|
317
|
+
switch (provider) {
|
|
318
|
+
case "openai":
|
|
319
|
+
return "Export OPENAI_API_KEY in your environment.";
|
|
320
|
+
case "anthropic":
|
|
321
|
+
return "Export ANTHROPIC_API_KEY in your environment.";
|
|
322
|
+
case "google":
|
|
323
|
+
return "Export GOOGLE_API_KEY (or GEMINI_API_KEY) in your environment.";
|
|
324
|
+
default:
|
|
325
|
+
return "";
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function detectShallowReview(
|
|
330
|
+
content: string,
|
|
331
|
+
minWeaknesses: number,
|
|
332
|
+
): string | undefined {
|
|
333
|
+
const fileRefs = content.match(/[\w./_-]+\.(ts|tsx|js|jsx|py|go|rb|md|json|yaml|yml)(?::\d+)?/g);
|
|
334
|
+
const fileRefCount = fileRefs ? fileRefs.length : 0;
|
|
335
|
+
|
|
336
|
+
const weaknessSection = content.match(
|
|
337
|
+
/##\s+Concrete\s+Weaknesses\s*\n([\s\S]*?)(\n##|$)/i,
|
|
338
|
+
);
|
|
339
|
+
const weaknessCount = weaknessSection
|
|
340
|
+
? (weaknessSection[1]!.match(/^\s*\d+\.\s+/gm) ?? []).length
|
|
341
|
+
: 0;
|
|
342
|
+
|
|
343
|
+
if (fileRefCount === 0) {
|
|
344
|
+
return `Reviewer output contains zero file/line references. Likely shallow.`;
|
|
345
|
+
}
|
|
346
|
+
if (weaknessCount < minWeaknesses) {
|
|
347
|
+
return (
|
|
348
|
+
`Reviewer listed ${weaknessCount} concrete weaknesses; config requires ${minWeaknesses}. ` +
|
|
349
|
+
`Output may be shallow.`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|