indusagi-coding-agent 0.2.12 → 0.2.14

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 (81) hide show
  1. package/.env.review.example +25 -0
  2. package/README.md +30 -0
  3. package/REVIEW_AGENT.md +137 -0
  4. package/dist/entry.js +1861 -979
  5. package/dist/guardrails.js +212 -36
  6. package/dist/index.js +2829 -1057
  7. package/dist/review-agent.js +4461 -0
  8. package/dist/review-server.js +4633 -0
  9. package/package.json +24 -7
  10. package/src/boot/auth-vault.ts +4 -5
  11. package/src/boot/contract.ts +1 -1
  12. package/src/boot/runners/addon-wiring.ts +111 -3
  13. package/src/boot/runners/benchmark-integrity.ts +107 -0
  14. package/src/boot/runners/repl-runner.ts +2 -1
  15. package/src/boot/runners/session.ts +26 -12
  16. package/src/boot/server-token.ts +20 -11
  17. package/src/boot/stages.ts +3 -6
  18. package/src/briefing/context-docs.ts +144 -12
  19. package/src/channels/oneshot.ts +6 -0
  20. package/src/conductor/catalog/catalog.ts +3 -1
  21. package/src/conductor/conductor.ts +77 -17
  22. package/src/conductor/contract.ts +9 -7
  23. package/src/conductor/index.ts +1 -1
  24. package/src/conductor/transcript-store/store.ts +4 -8
  25. package/src/console/auth-status.ts +9 -4
  26. package/src/console/components/AgentsView.ts +2 -2
  27. package/src/console/components/BackgroundAgents.ts +15 -2
  28. package/src/console/components/Banner.ts +20 -9
  29. package/src/console/components/Composer.ts +57 -82
  30. package/src/console/components/ScrollViewport.ts +46 -0
  31. package/src/console/components/TerminalConsole.ts +165 -162
  32. package/src/console/components/WorkingIndicator.ts +6 -5
  33. package/src/console/components/banner-sweep.ts +5 -1
  34. package/src/console/components/welcome.ts +3 -3
  35. package/src/console/contract.ts +10 -5
  36. package/src/console/index.ts +7 -7
  37. package/src/console/input/complete.ts +2 -2
  38. package/src/console/input/dir-reader.ts +3 -1
  39. package/src/console/input/graphemes.ts +22 -0
  40. package/src/console/input/keymap.ts +6 -4
  41. package/src/console/mount.ts +45 -3
  42. package/src/console/overlays/auth.ts +28 -8
  43. package/src/console/overlays/boards.ts +13 -40
  44. package/src/console/overlays/host.ts +10 -0
  45. package/src/console/overlays/pickers.ts +35 -30
  46. package/src/console/reducer.ts +12 -5
  47. package/src/console/slash/builtins.ts +4 -3
  48. package/src/console/slash/commands/integrations.ts +13 -14
  49. package/src/console/slash/commands/transcript.ts +3 -8
  50. package/src/console/slash/commands/workbench.ts +4 -4
  51. package/src/console/test-support.ts +48 -0
  52. package/src/index.ts +1 -0
  53. package/src/integrations/sarvam/config.ts +15 -13
  54. package/src/kit/external-editor.ts +17 -1
  55. package/src/kit/run-shell.ts +31 -0
  56. package/src/launch/catalog.ts +4 -2
  57. package/src/launch/contract.ts +1 -0
  58. package/src/launch/invocation/flags.ts +13 -1
  59. package/src/launch/invocation/read.ts +4 -3
  60. package/src/launch/login.ts +23 -10
  61. package/src/launch/oauth.ts +3 -0
  62. package/src/models.ts +166 -0
  63. package/src/review-agent/config.ts +75 -0
  64. package/src/review-agent/contract.ts +59 -0
  65. package/src/review-agent/github.ts +242 -0
  66. package/src/review-agent/index.ts +8 -0
  67. package/src/review-agent/prompt.ts +132 -0
  68. package/src/review-agent/reviewer.ts +118 -0
  69. package/src/review-agent/server.ts +221 -0
  70. package/src/review-agent/service.ts +165 -0
  71. package/src/review-agent/supabase.ts +209 -0
  72. package/src/review-agent/webhook.ts +76 -0
  73. package/src/security/private-state.ts +122 -0
  74. package/src/window-budget/summarize/condense.ts +4 -1
  75. package/src/workspace/brand.ts +1 -1
  76. package/src/workspace/locator.ts +2 -2
  77. package/supabase/migrations/20260827031803_indusagi_review_agent.sql +109 -0
  78. package/supabase/migrations/20260827035747_atomic_webhook_enqueue.sql +75 -0
  79. package/supabase/migrations/20260827040104_check_run_reconciliation.sql +8 -0
  80. package/supabase/migrations/20260827041049_owned_job_leases.sql +80 -0
  81. package/supabase/migrations/20260827042500_static_token_webhooks.sql +75 -0
@@ -9,7 +9,9 @@ import { basename } from "node:path";
9
9
  import type { InkThemeAdapter } from "indusagi/react-ink";
10
10
  import type { StartupChangelog, StartupMap, StartupNotice } from "../startup.js";
11
11
  import { MASCOTS, WHATS_NEW, pickWelcomeVariant, mascotRows, type MascotFace } from "./welcome.js";
12
- import { welcomeLine } from "./banner-sweep.js";
12
+ import { welcomeLine, rowGradient } from "./banner-sweep.js";
13
+ import chalk from "chalk";
14
+ import { useTerminalSize } from "./ScrollViewport.js";
13
15
 
14
16
  const BRAND = "IndusCode";
15
17
  const COMPACT_EMBLEM = "▛▜▙▟";
@@ -23,6 +25,11 @@ const WORDMARK_ROWS: readonly string[] = [
23
25
  "╚═╝╚═╝ ╚═══╝╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝"
24
26
  ];
25
27
 
28
+ function StartupSummary({theme, startup}: {theme: InkThemeAdapter; startup?: StartupMap}) {
29
+ if (!startup?.sections.length) return null;
30
+ return jsx(Text, {wrap:"truncate-end", children:theme.muted(startup.sections.map(section => `${section.title}: ${section.count}`).join(" · "))});
31
+ }
32
+
26
33
  function StartupNotices({ theme, notices }: { theme: InkThemeAdapter; notices: readonly StartupNotice[] }) {
27
34
  if (notices.length === 0) return null;
28
35
  return jsx(Box, {
@@ -98,10 +105,10 @@ function ShellLine({ theme, workspace, branch }: { theme: InkThemeAdapter; works
98
105
  });
99
106
  }
100
107
 
101
- function Wordmark({ theme }: { theme: InkThemeAdapter }) {
108
+ function Wordmark({ theme, sweep }: { theme: InkThemeAdapter; sweep?: boolean }) {
102
109
  return jsx(Box, {
103
110
  flexDirection: "column",
104
- children: WORDMARK_ROWS.map((line, index) => jsx(Text, { children: theme.color("accent", line) }, index))
111
+ children: WORDMARK_ROWS.map((line, index) => jsx(Text, { children: sweep ? chalk.hex(rowGradient(index, WORDMARK_ROWS.length, theme.colors.accent, theme.colors.customMessage))(line) : theme.color("accent", line) }, index))
105
112
  });
106
113
  }
107
114
 
@@ -127,7 +134,7 @@ function paintMascot(theme: InkThemeAdapter, tone: string, text: string) {
127
134
  }
128
135
  }
129
136
 
130
- function Mascot({ theme, face }: { theme: InkThemeAdapter; face: MascotFace }) {
137
+ function Mascot({ theme, face, sweep }: { theme: InkThemeAdapter; face: MascotFace; sweep?: boolean }) {
131
138
  return jsx(Box, {
132
139
  flexDirection: "column",
133
140
  alignItems: "center",
@@ -137,7 +144,7 @@ function Mascot({ theme, face }: { theme: InkThemeAdapter; face: MascotFace }) {
137
144
  Box,
138
145
  {
139
146
  children: row.map((seg, segIndex) =>
140
- jsx(Text, { children: paintMascot(theme, seg.tone, seg.text) }, segIndex)
147
+ jsx(Text, { children: sweep && seg.tone === "head" ? chalk.hex(rowGradient(rowIndex, 5, theme.colors.accent, theme.colors.customMessage))(seg.text) : paintMascot(theme, seg.tone, seg.text) }, segIndex)
141
148
  )
142
149
  },
143
150
  rowIndex
@@ -174,6 +181,7 @@ export interface BannerProps {
174
181
  }
175
182
 
176
183
  export function Banner(props: BannerProps) {
184
+ const { columns } = useTerminalSize();
177
185
  const {
178
186
  theme,
179
187
  modelId,
@@ -187,15 +195,16 @@ export function Banner(props: BannerProps) {
187
195
  variantSeed,
188
196
  whatsNew,
189
197
  notices,
190
- changelog
198
+ changelog, sweep, startup
191
199
  } = props;
192
200
  const noticeList = notices ?? [];
193
- if (quiet || compact) {
201
+ if (quiet || compact || columns < 76) {
194
202
  return jsxs(Box, {
195
203
  flexDirection: "column",
196
204
  marginBottom: 1,
197
205
  children: [
198
206
  jsx(StartupNotices, { theme, notices: noticeList }),
207
+ jsx(StartupSummary, {theme, startup}),
199
208
  jsxs(Box, {
200
209
  children: [
201
210
  compact
@@ -225,8 +234,9 @@ export function Banner(props: BannerProps) {
225
234
  marginBottom: 1,
226
235
  children: [
227
236
  jsx(StartupNotices, { theme, notices: noticeList }),
237
+ jsx(StartupSummary, {theme, startup}),
228
238
  jsx(ShellLine, { theme, workspace, branch }),
229
- jsx(Box, { marginTop: 1, children: jsx(Wordmark, { theme }) }),
239
+ jsx(Box, { marginTop: 1, children: jsx(Wordmark, { theme, sweep }) }),
230
240
  jsxs(Box, {
231
241
  marginTop: 1,
232
242
  flexDirection: "column",
@@ -266,6 +276,7 @@ export function Banner(props: BannerProps) {
266
276
  marginBottom: 1,
267
277
  children: [
268
278
  jsx(StartupNotices, { theme, notices: noticeList }),
279
+ jsx(StartupSummary, {theme, startup}),
269
280
  jsxs(Box, {
270
281
  flexDirection: "column",
271
282
  borderStyle: "round",
@@ -292,7 +303,7 @@ export function Banner(props: BannerProps) {
292
303
  marginRight: 2,
293
304
  children: [
294
305
  jsx(Text, { bold: true, children: theme.color("text", greeting) }),
295
- jsx(Mascot, { theme, face: variant.mascot }),
306
+ jsx(Mascot, { theme, face: variant.mascot, sweep }),
296
307
  jsx(Text, { children: theme.dim(metaLine) }),
297
308
  jsx(Text, { children: theme.dim(shortenWorkspace(workspace)) })
298
309
  ]
@@ -1,29 +1,31 @@
1
- /**
2
- * Composer — the prompt-input surface.
3
- */
4
- import { Box, Text } from "indusagi/react-host/ink";
5
- import { jsx, jsxs } from "indusagi/react-host/jsx-runtime";
6
- import type { InkThemeAdapter } from "indusagi/react-ink";
7
- import type { Suggestion } from "../input/index.js";
1
+ /** Bounded, grapheme-aware editor and selection-following completion menu. */
2
+ import { Box, Text } from 'indusagi/react-host/ink';
3
+ import { jsx, jsxs } from 'indusagi/react-host/jsx-runtime';
4
+ import stringWidth from 'string-width';
5
+ import type { InkThemeAdapter } from 'indusagi/react-ink';
6
+ import type { Suggestion } from '../input/index.js';
7
+ import { snapGrapheme } from '../input/graphemes.js';
8
+ import { useTerminalSize } from './ScrollViewport.js';
8
9
 
9
- const PROMPT_GLYPH = "› ";
10
- const SUGGESTION_WINDOW = 8;
11
- const NAME_COLUMN_CAP = 24;
12
- const NAME_COLUMN_FLOOR = 8;
13
-
14
- function clampName(label: string, width: number): string {
15
- if (label.length <= width) return label;
16
- if (width <= 1) return label.slice(0, width);
17
- return `${label.slice(0, width - 1)}…`;
18
- }
19
-
20
- function bufferWithCaret(buffer: string, caret: number) {
21
- const before = buffer.slice(0, caret);
22
- const under = caret < buffer.length ? buffer[caret] : " ";
23
- const after = caret < buffer.length ? buffer.slice(caret + 1) : "";
24
- return jsxs(Text, {
25
- children: [before, jsx(Text, { inverse: true, children: under }), after]
26
- });
10
+ const segmenter = new Intl.Segmenter(undefined, {granularity:'grapheme'});
11
+ interface Cell { text: string; caret: boolean }
12
+ function editorLines(buffer: string, caret: number, width: number) {
13
+ const lines: Cell[][] = [[]];
14
+ let column = 0;
15
+ caret = snapGrapheme(buffer, caret);
16
+ for (const {segment, index} of segmenter.segment(buffer + ' ')) {
17
+ const cursor = index === caret;
18
+ if (segment === '\n') {
19
+ if (cursor) lines.at(-1)!.push({text:' ', caret:true});
20
+ lines.push([]); column=0; continue;
21
+ }
22
+ const text = segment === '\t' ? ' ' : segment.replace(/[\x00-\x1f\x7f]/g,'');
23
+ const cells = stringWidth(text);
24
+ if (column + cells > width && column > 0) {lines.push([]);column=0;}
25
+ lines.at(-1)!.push({text:cells > width ? '?' : text,caret:cursor});
26
+ column += Math.min(width,cells);
27
+ }
28
+ return lines;
27
29
  }
28
30
 
29
31
  export interface ComposerProps {
@@ -33,62 +35,35 @@ export interface ComposerProps {
33
35
  readonly busy: boolean;
34
36
  readonly suggestions: readonly Suggestion[];
35
37
  readonly highlight: number;
38
+ readonly paddingX?: number;
36
39
  }
37
40
 
38
- export function Composer({ theme, buffer, caret, busy, suggestions, highlight }: ComposerProps) {
39
- const shown = suggestions.slice(0, SUGGESTION_WINDOW);
40
- const borderColor = theme.colors.borderMuted ?? theme.colors.accent ?? "gray";
41
- const nameWidth = shown.length > 0
42
- ? Math.max(NAME_COLUMN_FLOOR, Math.min(NAME_COLUMN_CAP, Math.max(...shown.map((s) => s.label.length))))
43
- : 0;
44
- return jsxs(Box, {
45
- flexDirection: "column",
46
- children: [
47
- jsxs(Box, {
48
- borderStyle: "round",
49
- borderColor,
50
- paddingX: 1,
51
- children: [
52
- jsxs(Box, {
53
- flexGrow: 1,
54
- children: [
55
- jsx(Text, { children: theme.color("customMessage", PROMPT_GLYPH) }),
56
- bufferWithCaret(buffer, caret)
57
- ]
58
- }),
59
- jsx(Text, { children: theme.dim("↵ send") })
60
- ]
61
- }),
62
- shown.length > 0
63
- ? jsx(Box, {
64
- flexDirection: "column",
65
- marginTop: 1,
66
- children: shown.map((suggestion, index) => {
67
- const active = index === highlight;
68
- const name = clampName(suggestion.label, nameWidth);
69
- return jsxs(
70
- Box,
71
- {
72
- children: [
73
- jsx(Box, {
74
- width: 2,
75
- flexShrink: 0,
76
- children: jsx(Text, { children: active ? theme.color("accent", "▸") : " " })
77
- }),
78
- jsx(Box, {
79
- width: nameWidth,
80
- flexShrink: 0,
81
- children: jsx(Text, { bold: active, children: theme.color("customMessage", name) })
82
- }),
83
- jsx(Text, { children: " " }),
84
- jsx(Text, { children: theme.muted(suggestion.detail) })
85
- ]
86
- },
87
- suggestion.value
88
- );
89
- })
90
- })
91
- : null
92
- ]
93
- });
94
- }
41
+ export function Composer({theme,buffer,caret,suggestions,highlight,paddingX=1}: ComposerProps) {
42
+ const {columns,rows} = useTerminalSize();
43
+ const padding = Math.max(0, Math.min(3, Math.floor(paddingX), Math.floor((columns-8)/2)));
44
+ const sendWidth = columns >= 32 ? 7 : 0;
45
+ const width = Math.max(1,columns - 2 - padding*2 - 2 - sendWidth);
46
+ const allLines = editorLines(buffer,caret,width);
47
+ const caretRow = Math.max(0,allLines.findIndex(line=>line.some(cell=>cell.caret)));
48
+ const editorBudget = Math.max(1,Math.min(6,Math.floor(rows/4)));
49
+ const top = Math.max(0,Math.min(caretRow-editorBudget+1,allLines.length-editorBudget));
50
+ const lines = allLines.slice(top,top+editorBudget);
51
+ const budget = Math.max(1,Math.min(8,rows-lines.length-12));
52
+ const start = Math.max(0,Math.min(highlight-budget+1,suggestions.length-budget));
53
+ const shown = suggestions.slice(start,start+budget);
54
+ const nameWidth = Math.max(1,Math.min(24,Math.floor(columns/3)));
55
+ return jsxs(Box,{flexDirection:'column',flexShrink:0,children:[
56
+ jsx(Box,{borderStyle:'round',borderColor:theme.colors.borderMuted??'gray',paddingX:padding,flexDirection:'column',children:
57
+ lines.map((line,index)=>jsxs(Box,{height:1,flexShrink:0,children:[
58
+ jsx(Box,{width:2,flexShrink:0,children:jsx(Text,{children:theme.color('customMessage',index+top===0?'› ':'· ')})}),
59
+ jsx(Box,{width,flexShrink:0,children:jsx(Text,{wrap:'truncate-end',children:line.map((cell,i)=>jsx(Text,{inverse:cell.caret,children:cell.text},i))})}),
60
+ sendWidth ? jsx(Box,{width:sendWidth,flexShrink:0,children:jsx(Text,{children:index===0?theme.dim(' ↵ send'):''})}) : null
61
+ ]},index))
62
+ }),
63
+ shown.length ? jsx(Box,{flexDirection:'column',marginTop:1,children:shown.map((suggestion,index)=>jsxs(Box,{height:1,children:[
64
+ jsx(Box,{width:2,flexShrink:0,children:jsx(Text,{children:start+index===highlight?theme.color('accent','▸'):' '})}),
65
+ jsx(Box,{width:nameWidth,flexShrink:0,children:jsx(Text,{wrap:'truncate-end',bold:start+index===highlight,children:theme.color('customMessage',suggestion.label)})}),
66
+ jsx(Box,{width:Math.max(1,columns-nameWidth-2),children:jsx(Text,{wrap:'truncate-end',children:theme.muted(' '+suggestion.detail)})})
67
+ ]},suggestion.value))}) : null
68
+ ]});
69
+ }
@@ -0,0 +1,46 @@
1
+ import React, { useEffect, useState } from 'indusagi/react-host';
2
+ import { Box, useInput } from 'indusagi/react-host/ink';
3
+ import { measureElement, useStdout, type DOMElement } from 'ink';
4
+ import { jsx } from 'indusagi/react-host/jsx-runtime';
5
+ import type { ReactNode } from 'react';
6
+
7
+ export function useTerminalSize() {
8
+ const { stdout } = useStdout();
9
+ const read = () => ({ columns: Math.max(1, stdout.columns || 80), rows: Math.max(1, stdout.rows || 24) });
10
+ const [size, setSize] = useState(read);
11
+ useEffect(() => {
12
+ const resize = () => setSize(read());
13
+ stdout.on('resize', resize);
14
+ return () => { stdout.off('resize', resize); };
15
+ }, [stdout]);
16
+ return size;
17
+ }
18
+
19
+ /** Row-bounded Ink viewport. Page keys scroll; new output follows only at the bottom. */
20
+ export function ScrollViewport({ children, active = true, follow = true }: { children: ReactNode; active?: boolean; follow?: boolean }) {
21
+ const outer = React.useRef<DOMElement>(null);
22
+ const inner = React.useRef<DOMElement>(null);
23
+ const [metrics, setMetrics] = useState({ height: 1, content: 1 });
24
+ const [offset, setOffset] = useState(0);
25
+ useEffect(() => {
26
+ if (!outer.current || !inner.current) return;
27
+ const height = measureElement(outer.current).height;
28
+ const content = measureElement(inner.current).height;
29
+ if (metrics.height !== height || metrics.content !== content) {
30
+ const oldMax = Math.max(0, metrics.content - metrics.height);
31
+ const newMax = Math.max(0, content - height);
32
+ if (follow) setOffset(n => n === 0 ? 0 : Math.max(0, Math.min(newMax, n + newMax - oldMax)));
33
+ setMetrics({height, content});
34
+ }
35
+ });
36
+ const max = Math.max(0, metrics.content - metrics.height);
37
+ const scroll = Math.min(max, offset);
38
+ useInput((_input, key) => {
39
+ const step = Math.max(1, metrics.height - 2);
40
+ if (key.pageUp) setOffset(n => Math.min(max, n + (follow ? step : -step)));
41
+ if (key.pageDown) setOffset(n => Math.max(0, Math.min(max, n + (follow ? -step : step))));
42
+ }, {isActive: active});
43
+ const top = follow ? Math.max(0, max - scroll) : Math.max(0, scroll);
44
+ return jsx(Box, {ref: outer, flexDirection:'column', flexGrow:1, flexBasis:0, minHeight:1, overflow:'hidden',
45
+ children:jsx(Box, {ref:inner, flexDirection:'column', flexShrink:0, marginTop:-top, children})});
46
+ }