indusagi-coding-agent 0.2.12 → 0.2.13

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 (38) hide show
  1. package/.env.review.example +25 -0
  2. package/README.md +11 -0
  3. package/REVIEW_AGENT.md +137 -0
  4. package/dist/entry.js +101 -23
  5. package/dist/guardrails.js +67 -1
  6. package/dist/index.js +1032 -64
  7. package/dist/review-agent.js +4177 -0
  8. package/dist/review-server.js +4349 -0
  9. package/package.json +14 -6
  10. package/src/boot/contract.ts +1 -1
  11. package/src/boot/runners/session.ts +8 -4
  12. package/src/boot/stages.ts +3 -6
  13. package/src/conductor/catalog/catalog.ts +3 -1
  14. package/src/conductor/conductor.ts +10 -5
  15. package/src/conductor/contract.ts +5 -4
  16. package/src/conductor/index.ts +1 -1
  17. package/src/console/slash/commands/workbench.ts +2 -2
  18. package/src/index.ts +1 -0
  19. package/src/launch/catalog.ts +4 -2
  20. package/src/launch/contract.ts +1 -0
  21. package/src/launch/invocation/flags.ts +1 -1
  22. package/src/launch/invocation/read.ts +4 -3
  23. package/src/models.ts +90 -0
  24. package/src/review-agent/config.ts +75 -0
  25. package/src/review-agent/contract.ts +59 -0
  26. package/src/review-agent/github.ts +242 -0
  27. package/src/review-agent/index.ts +8 -0
  28. package/src/review-agent/prompt.ts +132 -0
  29. package/src/review-agent/reviewer.ts +118 -0
  30. package/src/review-agent/server.ts +221 -0
  31. package/src/review-agent/service.ts +165 -0
  32. package/src/review-agent/supabase.ts +209 -0
  33. package/src/review-agent/webhook.ts +76 -0
  34. package/supabase/migrations/20260827031803_indusagi_review_agent.sql +109 -0
  35. package/supabase/migrations/20260827035747_atomic_webhook_enqueue.sql +75 -0
  36. package/supabase/migrations/20260827040104_check_run_reconciliation.sql +8 -0
  37. package/supabase/migrations/20260827041049_owned_job_leases.sql +80 -0
  38. package/supabase/migrations/20260827042500_static_token_webhooks.sql +75 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indusagi-coding-agent",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "description": "Indusagi coding agent — a terminal-first AI coding agent, built from scratch on the indusagi framework.",
5
5
  "author": "Varun Israni",
6
6
  "license": "AGPL-3.0",
@@ -24,7 +24,8 @@
24
24
  "main": "./dist/index.js",
25
25
  "bin": {
26
26
  "indus": "dist/entry.js",
27
- "indusagi": "dist/entry.js"
27
+ "indusagi": "dist/entry.js",
28
+ "indusagi-review": "dist/review-server.js"
28
29
  },
29
30
  "exports": {
30
31
  ".": {
@@ -33,15 +34,21 @@
33
34
  "./guardrails": {
34
35
  "import": "./dist/guardrails.js"
35
36
  },
37
+ "./review-agent": {
38
+ "import": "./dist/review-agent.js"
39
+ },
36
40
  "./package.json": "./package.json"
37
41
  },
38
42
  "files": [
39
43
  "dist",
40
44
  "src",
45
+ "supabase",
46
+ ".env.review.example",
41
47
  "CHANGELOG.md",
42
48
  "CREDITS.md",
43
49
  "NOTICE",
44
- "README.md"
50
+ "README.md",
51
+ "REVIEW_AGENT.md"
45
52
  ],
46
53
  "scripts": {
47
54
  "typecheck": "tsc -p tsconfig.json --noEmit",
@@ -50,13 +57,14 @@
50
57
  "test:watch": "vitest",
51
58
  "prepublishOnly": "npm test && npm run build",
52
59
  "start": "node dist/entry.js",
53
- "start:help": "node dist/entry.js --help"
60
+ "start:help": "node dist/entry.js --help",
61
+ "review:server": "node dist/review-server.js"
54
62
  },
55
63
  "dependencies": {
56
64
  "@sinclair/typebox": "^0.34.49",
57
65
  "chalk": "^5.6.2",
58
66
  "highlight.js": "^11.11.1",
59
- "indusagi": "^0.13.10",
67
+ "indusagi": "0.13.11",
60
68
  "ink": "^5.2.1",
61
69
  "jiti": "^2.7.0",
62
70
  "marked": "^18.0.4",
@@ -72,4 +80,4 @@
72
80
  "typescript": "^5.7.2",
73
81
  "vitest": "^2.1.8"
74
82
  }
75
- }
83
+ }
@@ -131,7 +131,7 @@ export interface Invocation {
131
131
  readonly cwd?: string;
132
132
  /** Named credential account to authenticate with (`--account`). */
133
133
  readonly account?: string;
134
- /** Reasoning effort (`--thinking`): off / minimal / low / medium / high / xhigh. */
134
+ /** Reasoning effort (`--thinking`): off / minimal / low / medium / high / xhigh / max (model-dependent). */
135
135
  readonly thinking?: string;
136
136
  /** Replacement system prompt (`--system`); a path is read as a file, else literal text. */
137
137
  readonly system?: string;
@@ -58,6 +58,8 @@ import { createAuthVault } from "../auth-vault.js";
58
58
  import { TOKEN_FILE, hasValidServerToken, getServerToken } from "../server-token.js";
59
59
  import { resolveServerMode, resolveServerGatewayUrls, serverTierProviders } from "./server-mode.js";
60
60
  import { DiskMemoryStore, memoryDirFor, loadMemoryDoc } from "./memdir.js";
61
+ import { isReasoningEffortSupported } from "../../models.js";
62
+ import type { CodingAgentThinkingLevel } from "../../conductor/contract.js";
61
63
  import { buildAddonHost, wrapToolsWithAddons, concatAddonTools } from "./addon-wiring.js";
62
64
  import type { BootContext, Invocation } from "../contract.js";
63
65
 
@@ -219,10 +221,12 @@ function buildKeyResolver(
219
221
  };
220
222
  }
221
223
 
222
- const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh"]);
224
+ const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
223
225
 
224
- function resolveThinkingLevel(raw: string | undefined): string | undefined {
225
- return raw !== undefined && THINKING_LEVELS.has(raw) ? raw : undefined;
226
+ export function resolveThinkingLevel(raw: string | undefined, modelId?: string): CodingAgentThinkingLevel | undefined {
227
+ return raw !== undefined && THINKING_LEVELS.has(raw) && isReasoningEffortSupported(modelId, raw)
228
+ ? raw as CodingAgentThinkingLevel
229
+ : undefined;
226
230
  }
227
231
 
228
232
  function resolveText(value: string): string {
@@ -597,7 +601,7 @@ export async function buildSessionConductor(ctx: BootContext) {
597
601
  const tools = wrapToolsWithAddons(concatAddonTools(deck, addons), addons);
598
602
  const skills = gatherModelSkills(cwd);
599
603
  const system = composeSystem(tools, inv, cwd, skills, memoryDoc);
600
- const thinking = resolveThinkingLevel(inv.thinking);
604
+ const thinking = resolveThinkingLevel(inv.thinking, modelId);
601
605
  const fallbackModelId = inv.fallbackModelId;
602
606
  const planReachable = inv.mode === "repl";
603
607
  const canUseTool = buildPermissionGate(permissionRules, permissionMode, tools, planReachable);
@@ -29,6 +29,7 @@ import { tokenizeInvocation } from "./invocation.js";
29
29
  import { applyUpgrades } from "./upgrade/apply.js";
30
30
  import { ensureDirs } from "../workspace/locator.js";
31
31
  import type { BootContext, Stage } from "./contract.js";
32
+ import { createCodingAgentModelRegistry } from "../models.js";
32
33
 
33
34
  /**
34
35
  * Materialise the resolved workspace directories on disk.
@@ -110,11 +111,7 @@ async function loadDefaultSettings(): Promise<unknown> {
110
111
  }
111
112
 
112
113
  async function loadModelRegistry(): Promise<unknown> {
113
- const mod = (await import("indusagi/ai")) as { ModelRegistry?: unknown; modelRegistry?: unknown };
114
- if (typeof mod.ModelRegistry === "function") {
115
- return new (mod.ModelRegistry as new () => unknown)();
116
- }
117
- return mod.modelRegistry;
114
+ return createCodingAgentModelRegistry();
118
115
  }
119
116
 
120
117
  /**
@@ -164,4 +161,4 @@ export async function runStages(initial: BootContext, stages: readonly Stage[] =
164
161
  return ctx;
165
162
  }
166
163
 
167
- export { locateWorkspace, upgrade, buildInvocation, resolveResources, selectRunnerStage };
164
+ export { locateWorkspace, upgrade, buildInvocation, resolveResources, selectRunnerStage, loadModelRegistry };
@@ -1,7 +1,8 @@
1
1
  import { z } from "zod";
2
- import { getModels, getProviders } from "indusagi/ai";
2
+ import { getModels, getProviders, modelRegistry } from "indusagi/ai";
3
3
  import type { KnownProvider, Model } from "indusagi/ai";
4
4
  import type { ModelCardRef } from "../contract.js";
5
+ import { applyCodingAgentModelOverlay } from "../../models.js";
5
6
 
6
7
  export interface CatalogCard {
7
8
  readonly id: string;
@@ -29,6 +30,7 @@ const rawModelGate = z.object({
29
30
  });
30
31
 
31
32
  const EXCLUDED_PROVIDERS = new Set(["mock"]);
33
+ applyCodingAgentModelOverlay(modelRegistry);
32
34
  const frameworkSource: CatalogSource = {
33
35
  providers: () => getProviders().filter((provider) => !EXCLUDED_PROVIDERS.has(String(provider))),
34
36
  models: (provider) => getModels(provider as KnownProvider),
@@ -4,7 +4,7 @@ import { createZeroUsage } from "indusagi/ai";
4
4
  import type { Model, Usage } from "indusagi/ai";
5
5
  import * as path from "node:path";
6
6
  import { conductorFault } from "./contract.js";
7
- import type { ApprovalResolver, ConductorFault, ConductorPhase, ConductorState, PermissionMode, SessionConductor, SessionConductorOptions, SessionHead } from "./contract.js";
7
+ import type { ApprovalResolver, CodingAgentThinkingLevel, ConductorFault, ConductorPhase, ConductorState, PermissionMode, SessionConductor, SessionConductorOptions, SessionHead } from "./contract.js";
8
8
  import { ModelCatalog, ModelMatcher } from "./catalog/index.js";
9
9
  import type { CatalogCard } from "./catalog/index.js";
10
10
  import { SignalHub, translateAgentEvent } from "./signal-hub/index.js";
@@ -15,6 +15,7 @@ import { writePlan } from "../capability-deck/cards/plan-file.js";
15
15
  import { AUTO_CONDENSE_POLICY, isOverBudget } from "../window-budget/index.js";
16
16
  import { DiagnosticsEngine, formatDiagnosticsSummary, hasEslintConfig, hasTsConfig, isTypeScriptOrJs, runEslintDiagnostics, runTscDiagnostics } from "./diagnostics.js";
17
17
  import type { DiagnosticsConfig } from "./diagnostics.js";
18
+ import { isReasoningEffortSupported, type CodingAgentModel } from "../models.js";
18
19
 
19
20
  export interface AgentLike {
20
21
  subscribe(fn: (event: AgentEvent) => void): () => void;
@@ -62,7 +63,7 @@ export function reduceState(prev: ConductorState, action: StateAction): Conducto
62
63
  }
63
64
 
64
65
  const DEFAULT_RETRY = { maxAttempts: 2, baseDelayMs: 250 };
65
- const THINKING_LADDER: ThinkingLevel[] = ["off", "low", "medium", "high"];
66
+ const DEFAULT_THINKING_LADDER: readonly CodingAgentThinkingLevel[] = ["off", "low", "medium", "high"];
66
67
  const PERMISSION_CYCLE: PermissionMode[] = ["default", "acceptEdits", "plan", "bypass"];
67
68
 
68
69
  function newSessionId(): string { return `s_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`; }
@@ -73,6 +74,10 @@ function accumulateUsage(a: Usage, b: Usage): Usage {
73
74
  function errorText(error: unknown): string { return error instanceof Error ? error.message : String(error); }
74
75
  function isTransient(error: unknown): boolean { const text = errorText(error).toLowerCase(); return ["429", "rate limit", "overloaded", "timeout", "timed out", "econnreset", "etimedout", "503", "502", "500", "unavailable", "temporarily"].some((part) => text.includes(part)); }
75
76
  function isOverloadFault(error: unknown): boolean { const text = errorText(error).toLowerCase(); return text.includes("529") || text.includes("overload"); }
77
+ function thinkingLadderForModel(model: Model<any> | undefined): readonly CodingAgentThinkingLevel[] {
78
+ const efforts = (model as CodingAgentModel | undefined)?.reasoningEfforts;
79
+ return efforts?.length ? efforts : DEFAULT_THINKING_LADDER;
80
+ }
76
81
 
77
82
  export function withGatewayBaseUrl<M extends { baseUrl: string; provider: string }>(model: M, gatewayBaseUrls: Record<string, string> | undefined): M {
78
83
  const url = gatewayBaseUrls?.[model.provider];
@@ -132,7 +137,7 @@ class SessionConductorImpl implements SessionConductor {
132
137
  #draining = false;
133
138
  #queue: { mode: "steer" | "followUp"; text: string }[] = [];
134
139
  #boundModel?: Model<any>;
135
- #thinking: ThinkingLevel;
140
+ #thinking: CodingAgentThinkingLevel;
136
141
  #prePlanMode?: PermissionMode;
137
142
  #sessionName?: string;
138
143
  #gatewayBaseUrls?: Record<string, string>;
@@ -218,8 +223,8 @@ class SessionConductorImpl implements SessionConductor {
218
223
  stats() { const messages = this.agent.state.messages; let userMessages = 0, assistantMessages = 0, toolCalls = 0, toolResults = 0; for (const message of messages) { if (message.role === "user") userMessages++; else if (message.role === "assistant") { assistantMessages++; for (const part of message.content ?? []) if (typeof part === "object" && part && part.type === "toolCall") toolCalls++; } else if (message.role === "toolResult") toolResults++; } const usage = this.#state.usage; return { sessionId: this.#state.head.sessionId, userMessages, assistantMessages, toolCalls, toolResults, totalMessages: messages.length, tokens: { input: usage.input, output: usage.output, cacheRead: usage.cacheRead, cacheWrite: usage.cacheWrite, total: usage.totalTokens }, cost: usage.cost.total }; }
219
224
  costReport() { const stats = this.stats(); return [`Total cost: $${stats.cost.toFixed(4)}`, `Total tokens: ${stats.tokens.total}`, ` Input: ${stats.tokens.input}`, ` Output: ${stats.tokens.output}`, ` Cache read: ${stats.tokens.cacheRead}`, ` Cache write: ${stats.tokens.cacheWrite}`].join("\n"); }
220
225
  thinkingLevel() { return this.#thinking; }
221
- setThinkingLevel(level: ThinkingLevel) { this.#thinking = level; this.agent.setThinkingLevel?.(level); }
222
- cycleThinkingLevel() { const next = THINKING_LADDER[(THINKING_LADDER.indexOf(this.#thinking) + 1) % THINKING_LADDER.length]; this.setThinkingLevel(next); return next; }
226
+ setThinkingLevel(level: CodingAgentThinkingLevel) { if (!isReasoningEffortSupported(this.#boundModel?.id, level)) return; this.#thinking = level; this.agent.setThinkingLevel?.(level as ThinkingLevel); }
227
+ cycleThinkingLevel() { const ladder = thinkingLadderForModel(this.#boundModel); const next = ladder[(ladder.indexOf(this.#thinking) + 1) % ladder.length]; this.setThinkingLevel(next); return next; }
223
228
  permissionMode() { return this.modeHolder.mode; }
224
229
  setPermissionMode(mode: PermissionMode) { this.modeHolder.mode = mode; }
225
230
  setApprovalResolver(resolver: ApprovalResolver | undefined) { this.approvalHolder.resolver = resolver; }
@@ -40,6 +40,7 @@ import type { AgentMessage, AgentTool, CanUseToolFn, ThinkingLevel } from "indus
40
40
  import type { KnownProvider, Model, Usage } from "indusagi/ai";
41
41
  import type { PermissionMode } from "../settings.js";
42
42
  import type { ApprovalResolver } from "./permissions.js";
43
+ export type CodingAgentThinkingLevel = ThinkingLevel | "max";
43
44
  /** Re-exported framework vocabulary that conductor consumers routinely need. */
44
45
  export type { AgentMessage, AgentTool, CanUseToolFn, ThinkingLevel, Model, Usage, KnownProvider };
45
46
  /** Re-exported permission vocabulary, so conductor consumers get it in one import. */
@@ -434,7 +435,7 @@ export interface SessionConductorOptions {
434
435
  /** Tools made available to the agent for this session. */
435
436
  readonly tools?: AgentTool[];
436
437
  /** Initial reasoning effort for models that support it. */
437
- readonly thinking?: ThinkingLevel;
438
+ readonly thinking?: CodingAgentThinkingLevel;
438
439
  /**
439
440
  * Canonical id of a model to fall back to when the bound model is overloaded
440
441
  * (HTTP 529 / "overloaded") mid-turn (`--fallback-model`). When set, an
@@ -718,19 +719,19 @@ export interface SessionConductor {
718
719
  */
719
720
  costReport(): string;
720
721
  /** The reasoning effort currently applied to the session. */
721
- thinkingLevel(): ThinkingLevel;
722
+ thinkingLevel(): CodingAgentThinkingLevel;
722
723
  /**
723
724
  * Set the reasoning effort for subsequent turns. Applied to the agent when it
724
725
  * exposes a setter; otherwise stored and applied on the next model bind.
725
726
  *
726
727
  * @param level the reasoning effort to apply
727
728
  */
728
- setThinkingLevel(level: ThinkingLevel): void;
729
+ setThinkingLevel(level: CodingAgentThinkingLevel): void;
729
730
  /**
730
731
  * Advance the reasoning effort to the next level in the cycle, applying it, and
731
732
  * return the newly-selected level.
732
733
  */
733
- cycleThinkingLevel(): ThinkingLevel;
734
+ cycleThinkingLevel(): CodingAgentThinkingLevel;
734
735
  /**
735
736
  * The permission mode currently in effect. The live `canUseTool` gate reads
736
737
  * this on every tool call, so the value reflects the most recent
@@ -16,7 +16,7 @@
16
16
  * land; consumers import the conductor surface from `src/conductor` rather than
17
17
  * reaching into individual modules.
18
18
  */
19
- export type { FaultKind, ConductorFault, SessionSignal, SignalKind, SignalOf, SignalHandler, TranscriptSchema, TranscriptRole, TranscriptEntry, SessionHead, ModelCardRef, MatchQuery, ConductorPhase, ConductorState, QueueMode, QueuedInput, SessionStats, ExecuteBashOptions, BashOutcome, SessionConductor, SessionConductorOptions, CondenseOutcome, AgentMessage, AgentTool, CanUseToolFn, ThinkingLevel, Model, Usage, KnownProvider, PermissionMode, } from "./contract.js";
19
+ export type { FaultKind, ConductorFault, SessionSignal, SignalKind, SignalOf, SignalHandler, TranscriptSchema, TranscriptRole, TranscriptEntry, SessionHead, ModelCardRef, MatchQuery, ConductorPhase, ConductorState, QueueMode, QueuedInput, SessionStats, ExecuteBashOptions, BashOutcome, SessionConductor, SessionConductorOptions, CondenseOutcome, AgentMessage, AgentTool, CanUseToolFn, ThinkingLevel, CodingAgentThinkingLevel, Model, Usage, KnownProvider, PermissionMode, } from "./contract.js";
20
20
  export { conductorFault, TRANSCRIPT_SCHEMA } from "./contract.js";
21
21
  export { createSessionConductor, reduceState, noopCondense, withGatewayBaseUrl, type AgentLike, type ConductorDeps, type CondenseFn, type RetryPolicy, } from "./conductor.js";
22
22
  export { SignalHub, translateAgentEvent, type SignalHubOptions } from "./signal-hub/index.js";
@@ -147,7 +147,7 @@ const HOTKEYS: Array<[string, string]> = [
147
147
  ["Tab", "Accept the highlighted completion"],
148
148
  ["Ctrl+C", "Interrupt the running turn"],
149
149
  ["Ctrl+T", "Toggle reasoning / thinking rows"],
150
- ["/thinking", "Cycle thinking effort level (off → low → medium)"],
150
+ ["/thinking", "Cycle the selected model's supported reasoning efforts"],
151
151
  ["Ctrl+V", "Attach an image from the clipboard"],
152
152
  ["Ctrl+G", "Edit the composer in your external editor"],
153
153
  ["Ctrl+O", "Expand or collapse full tool output"],
@@ -232,7 +232,7 @@ export const workbenchCommands: SlashCommand[] = [
232
232
  },
233
233
  {
234
234
  name: "thinking",
235
- summary: "Cycle thinking effort level (off → low → medium → off)",
235
+ summary: "Cycle reasoning effort using the selected model's supported levels",
236
236
  run: (ctx) => {
237
237
  ctx.conductor.cycleThinkingLevel();
238
238
  return HANDLED;
package/src/index.ts CHANGED
@@ -15,4 +15,5 @@ export * as addons from "./addons/index.js";
15
15
  export * as channels from "./channels/index.js";
16
16
  export * as runtimeBridge from "./runtime-bridge/index.js";
17
17
  export * as insight from "./insight/index.js";
18
+ export * as reviewAgent from "./review-agent/index.js";
18
19
  export { VERSION } from "./workspace/index.js";
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { ModelRegistry, type KnownProvider, type Model } from "indusagi/ai";
5
5
  import type { CatalogFilter } from "./contract.js";
6
+ import { applyCodingAgentModelOverlay } from "../models.js";
6
7
 
7
8
  export interface CatalogIo {
8
9
  print(line: string): void;
@@ -22,10 +23,11 @@ export interface CatalogModelSource {
22
23
  }
23
24
 
24
25
  export function registrySource(registry: ModelRegistry = new ModelRegistry()): CatalogModelSource {
26
+ const overlaid = applyCodingAgentModelOverlay(registry);
25
27
  return {
26
- providers: (): readonly (KnownProvider | string)[] => registry.getProviders(),
28
+ providers: (): readonly (KnownProvider | string)[] => overlaid.getProviders(),
27
29
  models: (provider: string): readonly Model<Model<string>["api"]>[] =>
28
- registry.getModels(provider),
30
+ overlaid.getModels(provider),
29
31
  };
30
32
  }
31
33
 
@@ -33,6 +33,7 @@ export const THINKING_EFFORTS = [
33
33
  "medium",
34
34
  "high",
35
35
  "xhigh",
36
+ "max",
36
37
  ] as const;
37
38
 
38
39
  export type ThinkingEffort = (typeof THINKING_EFFORTS)[number];
@@ -20,7 +20,7 @@ export const FLAG_GROUPS: readonly { readonly id: FlagGroup; readonly title: str
20
20
  { id: "meta", title: "Meta" },
21
21
  ];
22
22
 
23
- const THINKING_CHOICES = ["off", "minimal", "low", "medium", "high", "xhigh"];
23
+ const THINKING_CHOICES = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
24
24
 
25
25
  export const FLAG_SPECS: readonly GroupedFlagSpec[] = [
26
26
  /* ---- Output ---------------------------------------------------------- */
@@ -9,6 +9,7 @@ import {
9
9
  isToolName,
10
10
  } from "../contract.js";
11
11
  import { FLAG_SPECS, type GroupedFlagSpec } from "./flags.js";
12
+ import { isReasoningEffortSupported } from "../../models.js";
12
13
 
13
14
  interface ParseState {
14
15
  flags: Record<string, FlagValue>;
@@ -159,9 +160,9 @@ function deriveMode(flags: Record<string, FlagValue>): Invocation["mode"] {
159
160
  return "text";
160
161
  }
161
162
 
162
- function resolveThinking(flags: Record<string, FlagValue>): Invocation["thinking"] {
163
+ function resolveThinking(flags: Record<string, FlagValue>, modelId: string | undefined): Invocation["thinking"] {
163
164
  const raw = readString(flags, "thinking");
164
- return raw !== undefined && isThinkingEffort(raw) ? raw : undefined;
165
+ return raw !== undefined && isThinkingEffort(raw) && isReasoningEffortSupported(modelId, raw) ? raw : undefined;
165
166
  }
166
167
 
167
168
  function resolveTools(flags: Record<string, FlagValue>): Invocation["tools"] {
@@ -215,7 +216,7 @@ export function readInvocation(argv: readonly string[]): Invocation {
215
216
  const cwd = readString(flags, "cwd");
216
217
  const system = readString(flags, "system");
217
218
  const appendSystem = readString(flags, "append-system");
218
- const thinking = resolveThinking(flags);
219
+ const thinking = resolveThinking(flags, model);
219
220
  const tools = resolveTools(flags);
220
221
  return {
221
222
  mode,
package/src/models.ts ADDED
@@ -0,0 +1,90 @@
1
+ import { ModelRegistry, type Api, type Model } from "indusagi/ai";
2
+
3
+ export type AstraReasoningLevel = "low" | "medium" | "high" | "xhigh" | "max";
4
+
5
+ export interface LongContextPricing {
6
+ readonly threshold: number;
7
+ readonly multipliers: {
8
+ readonly input: number;
9
+ readonly cacheRead: number;
10
+ readonly cacheWrite: number;
11
+ readonly output: number;
12
+ };
13
+ }
14
+
15
+ export interface CodingAgentModel<TApi extends Api = Api> extends Model<TApi> {
16
+ readonly reasoningEfforts?: readonly AstraReasoningLevel[];
17
+ readonly longContextPricing?: LongContextPricing;
18
+ }
19
+
20
+ const GPT_6_ASTRA_BASE = {
21
+ id: "gpt-6-astra",
22
+ name: "GPT-6 Astra",
23
+ api: "openai-responses",
24
+ reasoning: true,
25
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
26
+ input: ["text", "image"] as Array<"text" | "image">,
27
+ cost: {
28
+ input: 10,
29
+ cacheRead: 1,
30
+ cacheWrite: 12.5,
31
+ output: 50,
32
+ },
33
+ contextWindow: 1_050_000,
34
+ maxTokens: 128_000,
35
+ longContextPricing: {
36
+ threshold: 272_000,
37
+ multipliers: {
38
+ input: 2,
39
+ cacheRead: 2,
40
+ cacheWrite: 2,
41
+ output: 1.5,
42
+ },
43
+ },
44
+ } as const;
45
+
46
+ export const CODING_AGENT_MODELS: readonly CodingAgentModel[] = [
47
+ {
48
+ ...GPT_6_ASTRA_BASE,
49
+ provider: "openai",
50
+ baseUrl: "https://api.openai.com/v1",
51
+ },
52
+ {
53
+ ...GPT_6_ASTRA_BASE,
54
+ api: "openai-codex-responses",
55
+ provider: "openai-codex",
56
+ baseUrl: "https://chatgpt.com/backend-api",
57
+ },
58
+ {
59
+ ...GPT_6_ASTRA_BASE,
60
+ provider: "codex-cli",
61
+ baseUrl: "bridge:codex-cli",
62
+ },
63
+ {
64
+ ...GPT_6_ASTRA_BASE,
65
+ id: "openai/gpt-6-astra",
66
+ provider: "indusagi-cli",
67
+ baseUrl: "bridge:indusagi-cli",
68
+ },
69
+ {
70
+ ...GPT_6_ASTRA_BASE,
71
+ id: "openai-codex/gpt-6-astra",
72
+ api: "openai-codex-responses",
73
+ provider: "indusagi-cli",
74
+ baseUrl: "bridge:indusagi-cli",
75
+ },
76
+ ];
77
+
78
+ export function applyCodingAgentModelOverlay(registry: ModelRegistry): ModelRegistry {
79
+ for (const model of CODING_AGENT_MODELS) registry.registerCustomModel(model);
80
+ return registry;
81
+ }
82
+
83
+ export function createCodingAgentModelRegistry(): ModelRegistry {
84
+ return applyCodingAgentModelOverlay(new ModelRegistry());
85
+ }
86
+
87
+ export function isReasoningEffortSupported(modelId: string | undefined, effort: string): boolean {
88
+ if (modelId?.split("/").at(-1) !== "gpt-6-astra") return true;
89
+ return GPT_6_ASTRA_BASE.reasoningEfforts.includes(effort as AstraReasoningLevel);
90
+ }
@@ -0,0 +1,75 @@
1
+ const SDSDSD_SUPABASE_URL = "https://bnaadeovgcqgbrnrfcef.supabase.co";
2
+
3
+ export interface ReviewAgentConfig {
4
+ host: string;
5
+ port: number;
6
+ supabaseUrl: string;
7
+ supabaseServiceKey: string;
8
+ githubWebhookSecret: string;
9
+ githubToken?: string;
10
+ githubApp?: { appId: string; privateKey: string };
11
+ modelId: string;
12
+ pollIntervalMs: number;
13
+ maxJobsPerTick: number;
14
+ providerKeys: Readonly<Record<string, string>>;
15
+ }
16
+
17
+ const PROVIDER_ENV: Record<string, string> = {
18
+ anthropic: "ANTHROPIC_API_KEY",
19
+ openai: "OPENAI_API_KEY",
20
+ google: "GEMINI_API_KEY",
21
+ minimax: "MINIMAX_API_KEY",
22
+ mistral: "MISTRAL_API_KEY",
23
+ xai: "XAI_API_KEY",
24
+ openrouter: "OPENROUTER_API_KEY",
25
+ groq: "GROQ_API_KEY",
26
+ };
27
+
28
+ function required(env: NodeJS.ProcessEnv, name: string): string {
29
+ const value = env[name]?.trim();
30
+ if (!value) throw new Error(`${name} is required`);
31
+ return value;
32
+ }
33
+
34
+ function positiveInteger(value: string | undefined, fallback: number, name: string): number {
35
+ if (!value) return fallback;
36
+ const parsed = Number(value);
37
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${name} must be a positive integer`);
38
+ return parsed;
39
+ }
40
+
41
+ export function loadReviewAgentConfig(env: NodeJS.ProcessEnv = process.env): ReviewAgentConfig {
42
+ const supabaseServiceKey = required(env, "SUPABASE_SERVICE_ROLE_KEY");
43
+ const githubWebhookSecret = required(env, "GITHUB_WEBHOOK_SECRET");
44
+ const githubToken = env.GITHUB_TOKEN?.trim();
45
+ const appId = env.GITHUB_APP_ID?.trim();
46
+ const privateKey = env.GITHUB_APP_PRIVATE_KEY?.replace(/\\n/g, "\n").trim();
47
+ if (!githubToken && !(appId && privateKey)) {
48
+ throw new Error("Configure GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY, or GITHUB_TOKEN for development");
49
+ }
50
+
51
+ const modelId = env.INDUSAGI_REVIEW_MODEL?.trim() || "anthropic/claude-sonnet-4-6";
52
+ const provider = modelId.split("/", 1)[0];
53
+ const providerEnv = PROVIDER_ENV[provider];
54
+ if (providerEnv && !env[providerEnv]?.trim()) throw new Error(`${providerEnv} is required for ${modelId}`);
55
+
56
+ const providerKeys: Record<string, string> = {};
57
+ for (const [id, name] of Object.entries(PROVIDER_ENV)) {
58
+ const value = env[name]?.trim();
59
+ if (value) providerKeys[id] = value;
60
+ }
61
+
62
+ return {
63
+ host: env.REVIEW_AGENT_HOST?.trim() || env.HOST?.trim() || "0.0.0.0",
64
+ port: positiveInteger(env.REVIEW_AGENT_PORT ?? env.PORT, 8787, "REVIEW_AGENT_PORT"),
65
+ supabaseUrl: env.SUPABASE_URL?.trim().replace(/\/$/, "") || SDSDSD_SUPABASE_URL,
66
+ supabaseServiceKey,
67
+ githubWebhookSecret,
68
+ ...(githubToken ? { githubToken } : {}),
69
+ ...(appId && privateKey ? { githubApp: { appId, privateKey } } : {}),
70
+ modelId,
71
+ pollIntervalMs: positiveInteger(env.REVIEW_AGENT_POLL_MS ?? env.REVIEW_POLL_INTERVAL_MS, 2_000, "REVIEW_AGENT_POLL_MS"),
72
+ maxJobsPerTick: positiveInteger(env.REVIEW_AGENT_JOBS_PER_TICK ?? env.REVIEW_MAX_JOBS_PER_TICK, 5, "REVIEW_AGENT_JOBS_PER_TICK"),
73
+ providerKeys,
74
+ };
75
+ }
@@ -0,0 +1,59 @@
1
+ export const REVIEW_SEVERITIES = ["critical", "high", "medium", "low", "nit"] as const;
2
+ export const REVIEW_RISKS = ["low", "medium", "high"] as const;
3
+
4
+ export type ReviewSeverity = (typeof REVIEW_SEVERITIES)[number];
5
+ export type ReviewRisk = (typeof REVIEW_RISKS)[number];
6
+ export type ReviewSide = "LEFT" | "RIGHT";
7
+
8
+ export interface ChangedFile {
9
+ filename: string;
10
+ status: string;
11
+ additions: number;
12
+ deletions: number;
13
+ patch?: string;
14
+ }
15
+
16
+ export interface PullRequestContext {
17
+ repository: string;
18
+ pullNumber: number;
19
+ headSha: string;
20
+ baseSha?: string;
21
+ title: string;
22
+ description: string;
23
+ author: string;
24
+ additions: number;
25
+ deletions: number;
26
+ changedFiles: ChangedFile[];
27
+ diff: string;
28
+ }
29
+
30
+ export interface ReviewFinding {
31
+ severity: ReviewSeverity;
32
+ confidence: number;
33
+ title: string;
34
+ path: string;
35
+ line: number;
36
+ side: ReviewSide;
37
+ body: string;
38
+ suggestion?: string;
39
+ }
40
+
41
+ export interface ReviewOutput {
42
+ summary: string;
43
+ risk: ReviewRisk;
44
+ findings: ReviewFinding[];
45
+ testPlan: string[];
46
+ }
47
+
48
+ export interface ReviewRequest {
49
+ deliveryId: string;
50
+ action: string;
51
+ installationId: number;
52
+ repository: string;
53
+ owner: string;
54
+ repo: string;
55
+ pullNumber: number;
56
+ headSha: string;
57
+ title: string;
58
+ draft: boolean;
59
+ }