@twistezo/ai-review-council 2.1.0 → 3.0.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 +14 -9
- package/dist/cli-options.js +1 -10
- package/dist/cli.js +0 -1
- package/dist/domain/review-session.js +1 -1
- package/dist/execution/agent-events.js +1 -1
- package/dist/preflight/run-preflight.js +3 -4
- package/dist/providers/check-cli-status.js +3 -5
- package/dist/providers/claude-adapter.js +0 -1
- package/dist/providers/codex-adapter.js +0 -1
- package/dist/providers/copilot-adapter.js +0 -1
- package/dist/ui/agent-choice.js +5 -2
- package/dist/ui/app.js +5 -7
- package/dist/ui/describe-scope.js +6 -3
- package/dist/ui/steps/prompt-step.js +1 -1
- package/dist/ui/steps/reviewers-step.js +35 -43
- package/dist/ui/steps/run-screen.js +4 -3
- package/docs/council/council.md +2 -2
- package/docs/reviewer/code-review.md +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Docs:
|
|
|
11
11
|
- [CHANGELOG.md](CHANGELOG.md)
|
|
12
12
|
- [ARCHITECTURE.md](ARCHITECTURE.md)
|
|
13
13
|
|
|
14
|
-
## Supported
|
|
14
|
+
## Supported Agents
|
|
15
15
|
|
|
16
16
|
| Agent | CLI | Reviewer | Council | Models | Effort | Plan |
|
|
17
17
|
| ------- | ------------------------ | :------: | :-----: | ------ | ------- | ----------------------------------- |
|
|
@@ -38,17 +38,16 @@ CLIs must be available on `PATH` and logged in. Usage and availability depend on
|
|
|
38
38
|
npm install -g @twistezo/ai-review-council
|
|
39
39
|
|
|
40
40
|
# run with optional args
|
|
41
|
-
ai-review-council --dir ~/projects/my-app --lang pl --scope codebase
|
|
41
|
+
ai-review-council --dir ~/projects/my-app --lang pl --scope codebase
|
|
42
42
|
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
| Option
|
|
46
|
-
|
|
|
47
|
-
| `-d, --dir`
|
|
48
|
-
| `-l, --lang`
|
|
49
|
-
| `-s, --scope`
|
|
50
|
-
| `-
|
|
51
|
-
| `-t, --timeout` | minutes per agent, 1 to 30 | `10` |
|
|
45
|
+
| Option | Value | Default |
|
|
46
|
+
| --------------- | -------------------------------------- | ------- |
|
|
47
|
+
| `-d, --dir` | folder inside a Git repository | - |
|
|
48
|
+
| `-l, --lang` | `en`, `pl` | `en` |
|
|
49
|
+
| `-s, --scope` | `uncommitted`, `codebase`, commit hash | - |
|
|
50
|
+
| `-t, --timeout` | minutes per agent, 1 to 30 | `10` |
|
|
52
51
|
|
|
53
52
|
## Develop
|
|
54
53
|
|
|
@@ -59,3 +58,9 @@ npm test
|
|
|
59
58
|
npm run typecheck
|
|
60
59
|
npm run build # dist/
|
|
61
60
|
```
|
|
61
|
+
|
|
62
|
+
## Dev-Docs
|
|
63
|
+
|
|
64
|
+
[`dev-docs/`](dev-docs) is the domain memory for agents and people. Written in the [Matt Pocock skills](https://github.com/mattpocock/skills) style:
|
|
65
|
+
|
|
66
|
+
- [`dev-docs/GLOSSARY.md`](dev-docs/GLOSSARY.md): Generated and kept up to date with the `domain-modeling` skill.
|
package/dist/cli-options.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
|
-
import { z } from 'zod';
|
|
3
2
|
import { isCommitHashFormat } from './git/git-repository.js';
|
|
4
|
-
import { defaultTimeoutMinutes,
|
|
3
|
+
import { defaultTimeoutMinutes, maxTimeoutMinutes, reportLanguageSchema, reportLanguages, timeoutMinutesSchema, } from './domain/review-session.js';
|
|
5
4
|
export const usage = `Usage: ai-review-council [options]
|
|
6
5
|
|
|
7
6
|
Options:
|
|
@@ -9,7 +8,6 @@ Options:
|
|
|
9
8
|
-l, --lang <code> Report language: ${reportLanguages.join(', ')}; skips the language step
|
|
10
9
|
-s, --scope <value> uncommitted, codebase for the entire codebase, or a commit hash to review the changes after it;
|
|
11
10
|
skips the scope step
|
|
12
|
-
-r, --reviewers <n> Number of reviewers, 1 to ${maxReviewers}; skips the count question
|
|
13
11
|
-t, --timeout <min> Minutes each agent may run before it fails (default ${defaultTimeoutMinutes}, max ${maxTimeoutMinutes})
|
|
14
12
|
-h, --help Show this help`;
|
|
15
13
|
const readRawOptions = (args) => parseArgs({
|
|
@@ -18,7 +16,6 @@ const readRawOptions = (args) => parseArgs({
|
|
|
18
16
|
dir: { type: 'string', short: 'd' },
|
|
19
17
|
lang: { type: 'string', short: 'l' },
|
|
20
18
|
scope: { type: 'string', short: 's' },
|
|
21
|
-
reviewers: { type: 'string', short: 'r' },
|
|
22
19
|
timeout: { type: 'string', short: 't' },
|
|
23
20
|
help: { type: 'boolean', short: 'h' },
|
|
24
21
|
},
|
|
@@ -27,7 +24,6 @@ const readRawOptions = (args) => parseArgs({
|
|
|
27
24
|
}).values;
|
|
28
25
|
const uncommittedScope = 'uncommitted';
|
|
29
26
|
const codebaseScope = 'codebase';
|
|
30
|
-
const reviewerCountSchema = z.number().int().min(1).max(maxReviewers);
|
|
31
27
|
const parseScope = (value) => {
|
|
32
28
|
if (value === undefined) {
|
|
33
29
|
return null;
|
|
@@ -60,10 +56,6 @@ export const parseCliOptions = (args) => {
|
|
|
60
56
|
if (scope === 'invalid') {
|
|
61
57
|
return { ok: false, error: 'The --scope option needs `uncommitted`, `codebase` or a commit hash (4 to 40 hexadecimal characters).' };
|
|
62
58
|
}
|
|
63
|
-
const reviewerCount = values.reviewers === undefined ? null : reviewerCountSchema.safeParse(Number(values.reviewers));
|
|
64
|
-
if (reviewerCount !== null && !reviewerCount.success) {
|
|
65
|
-
return { ok: false, error: `The --reviewers option needs a whole number from 1 to ${maxReviewers}.` };
|
|
66
|
-
}
|
|
67
59
|
const timeout = timeoutMinutesSchema.safeParse(values.timeout === undefined ? defaultTimeoutMinutes : Number(values.timeout));
|
|
68
60
|
if (!timeout.success) {
|
|
69
61
|
return { ok: false, error: `The --timeout option needs whole minutes from 1 to ${maxTimeoutMinutes}.` };
|
|
@@ -74,7 +66,6 @@ export const parseCliOptions = (args) => {
|
|
|
74
66
|
directory: values.dir ?? null,
|
|
75
67
|
language: language === null ? null : language.data,
|
|
76
68
|
scope,
|
|
77
|
-
reviewerCount: reviewerCount === null ? null : reviewerCount.data,
|
|
78
69
|
timeoutMinutes: timeout.data,
|
|
79
70
|
showHelp: values.help ?? false,
|
|
80
71
|
},
|
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,6 @@ const main = async () => {
|
|
|
25
25
|
language: parsed.options.language,
|
|
26
26
|
directory: parsed.options.directory,
|
|
27
27
|
scope: parsed.options.scope,
|
|
28
|
-
reviewerCount: parsed.options.reviewerCount,
|
|
29
28
|
}, timeoutMinutes: parsed.options.timeoutMinutes, onFinish: (outcome) => {
|
|
30
29
|
result.outcome = outcome;
|
|
31
30
|
} }));
|
|
@@ -23,7 +23,7 @@ const agentConfigurationSchema = z
|
|
|
23
23
|
})
|
|
24
24
|
.refine((configuration) => configuration.provider !== 'copilot' ||
|
|
25
25
|
(configuration.model === copilotModelId && configuration.effort === null), { message: 'Copilot supports only Auto without reasoning effort.' });
|
|
26
|
-
export const maxReviewers =
|
|
26
|
+
export const maxReviewers = 10;
|
|
27
27
|
const reviewerAssignmentSchema = z.object({
|
|
28
28
|
configuration: agentConfigurationSchema,
|
|
29
29
|
reviewType: reviewTypeSchema,
|
|
@@ -173,7 +173,7 @@ export const parseCopilotLine = (line) => {
|
|
|
173
173
|
? { kind: 'completed', text: null }
|
|
174
174
|
: {
|
|
175
175
|
kind: 'error',
|
|
176
|
-
message: `Copilot exited with code ${exitCode ?? 'unknown'}. Check Copilot
|
|
176
|
+
message: `Copilot exited with code ${exitCode ?? 'unknown'}. Check Copilot usage limits.`,
|
|
177
177
|
},
|
|
178
178
|
];
|
|
179
179
|
default:
|
|
@@ -8,7 +8,7 @@ const checkGit = async (git) => {
|
|
|
8
8
|
label: 'Git',
|
|
9
9
|
state: 'failed',
|
|
10
10
|
detail: '`git` was not found in PATH. Git is required.',
|
|
11
|
-
fix:
|
|
11
|
+
fix: null,
|
|
12
12
|
}
|
|
13
13
|
: { label: 'Git', state: 'ok', detail: version, fix: null };
|
|
14
14
|
};
|
|
@@ -38,12 +38,11 @@ const checkProvider = async (adapter) => {
|
|
|
38
38
|
if (models.length === 0) {
|
|
39
39
|
return unavailable('The CLI returned no models.', 'Check your plan and CLI configuration.');
|
|
40
40
|
}
|
|
41
|
-
const loginNote = status.loginChecked ? '' : '; login checked when the review starts';
|
|
42
41
|
return {
|
|
43
42
|
check: {
|
|
44
43
|
label: adapter.displayName,
|
|
45
44
|
state: 'ok',
|
|
46
|
-
detail: `${status.version ?? ''}, ${models.length} models
|
|
45
|
+
detail: `${status.version ?? ''}, ${models.length} models`,
|
|
47
46
|
fix: null,
|
|
48
47
|
},
|
|
49
48
|
models,
|
|
@@ -65,7 +64,7 @@ export const runPreflight = async ({ git, adapters, findMissingInstructionFiles,
|
|
|
65
64
|
instructionsCheck.state === 'failed' ? 'Instruction files are missing.' : null,
|
|
66
65
|
models.some((model) => isCouncilProvider(model.provider))
|
|
67
66
|
? null
|
|
68
|
-
: 'At least one of Codex or Claude must be
|
|
67
|
+
: 'At least one of Codex or Claude must be available.',
|
|
69
68
|
].filter((blocker) => blocker !== null);
|
|
70
69
|
return {
|
|
71
70
|
checks: [gitCheck, ...providerChecks.map((result) => result.check), instructionsCheck],
|
|
@@ -9,7 +9,6 @@ export const checkCliStatus = async (run, check) => {
|
|
|
9
9
|
version: null,
|
|
10
10
|
problem: `\`${check.command}\` was not found in PATH.`,
|
|
11
11
|
fix: check.installFix,
|
|
12
|
-
loginChecked: false,
|
|
13
12
|
};
|
|
14
13
|
}
|
|
15
14
|
if (!isSuccessful(versionResult)) {
|
|
@@ -18,16 +17,15 @@ export const checkCliStatus = async (run, check) => {
|
|
|
18
17
|
version: null,
|
|
19
18
|
problem: `\`${check.command} ${versionArgs.join(' ')}\` failed: ${versionResult.stderr.trim()}`,
|
|
20
19
|
fix: check.installFix,
|
|
21
|
-
loginChecked: false,
|
|
22
20
|
};
|
|
23
21
|
}
|
|
24
22
|
const version = versionResult.stdout.trim();
|
|
25
23
|
if (check.loginStatusArgs === null) {
|
|
26
|
-
return { provider: check.provider, version, problem: null, fix: null
|
|
24
|
+
return { provider: check.provider, version, problem: null, fix: null };
|
|
27
25
|
}
|
|
28
26
|
const loginResult = await run(check.command, check.loginStatusArgs);
|
|
29
27
|
if (!isSuccessful(loginResult)) {
|
|
30
|
-
return { provider: check.provider, version, problem: '
|
|
28
|
+
return { provider: check.provider, version, problem: 'Provider is unavailable.', fix: null };
|
|
31
29
|
}
|
|
32
|
-
return { provider: check.provider, version, problem: null, fix: null
|
|
30
|
+
return { provider: check.provider, version, problem: null, fix: null };
|
|
33
31
|
};
|
|
@@ -51,7 +51,6 @@ export const createClaudeAdapter = (run, startProcess) => ({
|
|
|
51
51
|
command: 'claude',
|
|
52
52
|
loginStatusArgs: ['auth', 'status'],
|
|
53
53
|
installFix: 'Install Claude Code and make sure `claude` is on PATH, then run this app again.',
|
|
54
|
-
loginFix: 'Run `claude` in a terminal and sign in with `/login`, then run this app again.',
|
|
55
54
|
}),
|
|
56
55
|
// Catalog from the initialize control response; no user message is sent, so no model is called.
|
|
57
56
|
// Runs in the temp directory so project settings of the current folder are not loaded.
|
|
@@ -55,7 +55,6 @@ export const createCodexAdapter = (run, startProcess, client) => ({
|
|
|
55
55
|
command: 'codex',
|
|
56
56
|
loginStatusArgs: ['login', 'status'],
|
|
57
57
|
installFix: 'Install the Codex CLI and make sure `codex` is on PATH, then run this app again.',
|
|
58
|
-
loginFix: 'Run `codex login` in a terminal, then run this app again.',
|
|
59
58
|
}),
|
|
60
59
|
// Catalog over the local App Server protocol; no prompt is sent to a model.
|
|
61
60
|
listModels: async () => {
|
|
@@ -19,7 +19,6 @@ export const createCopilotAdapter = (run) => {
|
|
|
19
19
|
versionArgs,
|
|
20
20
|
loginStatusArgs: null,
|
|
21
21
|
installFix: 'Install Copilot CLI or GitHub CLI with `gh copilot` support, then run this app again.',
|
|
22
|
-
loginFix: command === 'gh' ? 'Run `gh copilot -- login`.' : 'Run `copilot login`.',
|
|
23
22
|
});
|
|
24
23
|
},
|
|
25
24
|
// Free can use Auto even when the named-model catalog is empty.
|
package/dist/ui/agent-choice.js
CHANGED
|
@@ -33,5 +33,8 @@ export const choiceToConfiguration = (models, choice) => {
|
|
|
33
33
|
const model = models[choice.modelIndex];
|
|
34
34
|
return model === undefined ? null : toConfiguration(model, choice.effort);
|
|
35
35
|
};
|
|
36
|
-
//
|
|
37
|
-
export const
|
|
36
|
+
// The reviewer at a zero-based position cycles through the review types.
|
|
37
|
+
export const newReviewer = (configuration, position) => ({
|
|
38
|
+
configuration,
|
|
39
|
+
reviewType: reviewTypes[position % reviewTypes.length] ?? 'code',
|
|
40
|
+
});
|
package/dist/ui/app.js
CHANGED
|
@@ -28,7 +28,7 @@ const buildSessionDraft = (state, timeoutMinutes) => {
|
|
|
28
28
|
},
|
|
29
29
|
scope: state.scope,
|
|
30
30
|
council: state.council,
|
|
31
|
-
reviewers: state.reviewers,
|
|
31
|
+
reviewers: state.reviewers.accepted,
|
|
32
32
|
prompt: state.prompt,
|
|
33
33
|
timeoutMinutes,
|
|
34
34
|
});
|
|
@@ -51,7 +51,7 @@ const answersBefore = (state) => {
|
|
|
51
51
|
repository: isBefore('project') ? state.repository : null,
|
|
52
52
|
scope: isBefore('scope') ? state.scope : null,
|
|
53
53
|
council: isBefore('council') ? state.council : null,
|
|
54
|
-
reviewers: isBefore('reviewers') ? state.reviewers : null,
|
|
54
|
+
reviewers: isBefore('reviewers') ? state.reviewers.accepted : null,
|
|
55
55
|
prompt: isBefore('prompt') ? state.prompt : null,
|
|
56
56
|
};
|
|
57
57
|
};
|
|
@@ -64,7 +64,6 @@ export const App = ({ title, services, presets, timeoutMinutes, onFinish }) => {
|
|
|
64
64
|
const { exit } = useApp();
|
|
65
65
|
const [state, setState] = useState({
|
|
66
66
|
step: 'preflight',
|
|
67
|
-
isGoingBack: false,
|
|
68
67
|
skipped: presetSkips(presets),
|
|
69
68
|
checks: [],
|
|
70
69
|
models: [],
|
|
@@ -73,7 +72,7 @@ export const App = ({ title, services, presets, timeoutMinutes, onFinish }) => {
|
|
|
73
72
|
repository: null,
|
|
74
73
|
scope: null,
|
|
75
74
|
council: null,
|
|
76
|
-
reviewers: [],
|
|
75
|
+
reviewers: { accepted: [], draft: null },
|
|
77
76
|
prompt: '',
|
|
78
77
|
running: null,
|
|
79
78
|
});
|
|
@@ -86,13 +85,12 @@ export const App = ({ title, services, presets, timeoutMinutes, onFinish }) => {
|
|
|
86
85
|
...previous,
|
|
87
86
|
...patch,
|
|
88
87
|
step: nextStep(previous.step, previous.skipped),
|
|
89
|
-
isGoingBack: false,
|
|
90
88
|
}));
|
|
91
89
|
};
|
|
92
90
|
const previous = previousStep(state.step, state.skipped);
|
|
93
91
|
const back = previous === null
|
|
94
92
|
? undefined
|
|
95
|
-
: () => setState((current) => ({ ...current, step: previous
|
|
93
|
+
: () => setState((current) => ({ ...current, step: previous }));
|
|
96
94
|
// A preset step that was skipped is shown when its value turned out invalid.
|
|
97
95
|
const isAutoSubmit = state.step === 'project' && state.skipped.includes('project');
|
|
98
96
|
const renderStep = () => {
|
|
@@ -108,7 +106,7 @@ export const App = ({ title, services, presets, timeoutMinutes, onFinish }) => {
|
|
|
108
106
|
case 'council':
|
|
109
107
|
return (_jsx(CouncilStep, { models: state.models.filter((model) => isCouncilProvider(model.provider)), value: state.council, onSubmit: (council) => complete({ council }), onBack: back }));
|
|
110
108
|
case 'reviewers':
|
|
111
|
-
return (_jsx(ReviewersStep, { models: state.models, value: state.reviewers,
|
|
109
|
+
return (_jsx(ReviewersStep, { models: state.models, value: state.reviewers, onChange: (reviewers) => setState((current) => ({ ...current, reviewers })), onSubmit: (accepted) => complete({ reviewers: { accepted, draft: null } }), onBack: back }));
|
|
112
110
|
case 'prompt':
|
|
113
111
|
return (_jsx(PromptStep, { value: state.prompt, readClipboard: services.readClipboard, onSubmit: (prompt) => complete({ prompt }), onBack: back }));
|
|
114
112
|
case 'summary': {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const
|
|
1
|
+
export const fileNoun = (count) => (count === 1 ? 'file' : 'files');
|
|
2
|
+
export const describeScopeRange = (scope) => {
|
|
2
3
|
const base = scope.base.hash.slice(0, 7);
|
|
3
4
|
switch (scope.kind) {
|
|
4
5
|
case 'uncommitted':
|
|
@@ -10,6 +11,8 @@ const describeRange = (scope) => {
|
|
|
10
11
|
}
|
|
11
12
|
};
|
|
12
13
|
export const describeScope = (scope) => {
|
|
13
|
-
const
|
|
14
|
-
|
|
14
|
+
const changedCount = scope.changedFiles.length;
|
|
15
|
+
const excludedCount = scope.excludedFiles.length;
|
|
16
|
+
const excluded = excludedCount === 0 ? '' : `, ${excludedCount} likely secret ${fileNoun(excludedCount)} excluded`;
|
|
17
|
+
return `${describeScopeRange(scope)} (${changedCount} ${fileNoun(changedCount)}${excluded})`;
|
|
15
18
|
};
|
|
@@ -5,5 +5,5 @@ import { MultilineInput } from '../components/multiline-input.js';
|
|
|
5
5
|
import { StepFrame } from '../components/step-frame.js';
|
|
6
6
|
export const PromptStep = ({ value, readClipboard, onSubmit, onBack }) => {
|
|
7
7
|
const [prompt, setPrompt] = useState(value);
|
|
8
|
-
return (_jsxs(StepFrame, { title: "Review prompt", hints: "Enter
|
|
8
|
+
return (_jsxs(StepFrame, { title: "Review prompt", hints: "Enter newline \u00B7 Ctrl+D confirm \u00B7 Cmd+V paste", onBack: onBack, children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Review context, goals and skills. Shared by all agents." }), _jsx(Text, { dimColor: true, children: "Read-only local review. No live web access." })] }), _jsx(MultilineInput, { value: prompt, onChange: setPrompt, onSubmit: () => onSubmit(prompt.trim()), readClipboard: readClipboard, placeholder: "Type or paste (Ctrl+V) the review prompt" })] }));
|
|
9
9
|
};
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { maxReviewers } from '../../domain/review-session.js';
|
|
5
|
-
import { choiceFromConfiguration, choiceToConfiguration,
|
|
5
|
+
import { choiceFromConfiguration, choiceToConfiguration, newReviewer } from '../agent-choice.js';
|
|
6
6
|
import { AgentPicker } from '../components/agent-picker.js';
|
|
7
7
|
import { ReviewerList } from '../components/reviewer-list.js';
|
|
8
|
-
import { SelectList } from '../components/select-list.js';
|
|
9
8
|
import { StepFrame } from '../components/step-frame.js';
|
|
10
9
|
import { theme } from '../theme.js';
|
|
11
10
|
// Keyed by reviewer index so the column focus resets for each reviewer.
|
|
@@ -16,52 +15,45 @@ const ReviewerEditor = ({ models, reviewer, onChange, onSubmit }) => {
|
|
|
16
15
|
if (configuration !== null) {
|
|
17
16
|
onChange({ ...reviewer, configuration });
|
|
18
17
|
}
|
|
19
|
-
}, reviewType: {
|
|
18
|
+
}, reviewType: {
|
|
19
|
+
value: reviewer.reviewType,
|
|
20
|
+
onChange: (reviewType) => onChange({ ...reviewer, reviewType }),
|
|
21
|
+
}, onSubmit: onSubmit }));
|
|
20
22
|
};
|
|
21
23
|
// A new reviewer starts from a clean choice: the first model with its default effort.
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
return
|
|
24
|
+
const freshReviewer = (models, position) => {
|
|
25
|
+
const configuration = choiceToConfiguration(models, choiceFromConfiguration(models, null));
|
|
26
|
+
return configuration === null ? null : newReviewer(configuration, position);
|
|
25
27
|
};
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
const [
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
// The next draft, or null when the accepted reviewers reach the limit.
|
|
29
|
+
const nextDraft = (models, accepted) => accepted.length < maxReviewers ? freshReviewer(models, accepted.length) : null;
|
|
30
|
+
export const ReviewersStep = ({ models, value, onChange, onSubmit, onBack }) => {
|
|
31
|
+
// coming back from the prompt, every accepted reviewer stays and a new one opens
|
|
32
|
+
const [selection, setSelection] = useState(() => ({
|
|
33
|
+
accepted: value.accepted,
|
|
34
|
+
draft: value.draft ?? nextDraft(models, value.accepted),
|
|
35
|
+
}));
|
|
36
|
+
const { accepted, draft } = selection;
|
|
37
|
+
const isAtLimit = accepted.length === maxReviewers;
|
|
38
|
+
const updateSelection = (next) => {
|
|
39
|
+
setSelection(next);
|
|
40
|
+
onChange(next);
|
|
41
|
+
};
|
|
42
|
+
useInput((input, key) => {
|
|
43
|
+
if (key.ctrl && input === 'd' && accepted.length > 0) {
|
|
44
|
+
onSubmit(accepted);
|
|
33
45
|
}
|
|
34
|
-
return presetCount === null ? null : 0;
|
|
35
46
|
});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
})), initialKey: String(Math.max(1, value.length)), onSelect: (key) => {
|
|
41
|
-
setReviewers((previous) => resize(models, previous, Number(key)));
|
|
42
|
-
setEditing(0);
|
|
43
|
-
} })] }));
|
|
44
|
-
}
|
|
45
|
-
const reviewer = reviewers[editing];
|
|
46
|
-
if (reviewer === undefined) {
|
|
47
|
-
return _jsx(Text, { color: theme.failure, children: "Reviewer not found." });
|
|
48
|
-
}
|
|
49
|
-
return (_jsxs(StepFrame, { title: `Reviewer ${editing + 1} of ${reviewers.length}`, hints: "\u2191\u2193 row \u00B7 \u2190\u2192 column \u00B7 Enter confirm", onBack: () => {
|
|
50
|
-
if (editing > 0) {
|
|
51
|
-
setEditing(editing - 1);
|
|
52
|
-
}
|
|
53
|
-
else if (presetCount === null) {
|
|
54
|
-
setEditing(null);
|
|
47
|
+
return (_jsxs(StepFrame, { title: isAtLimit ? 'Reviewers' : `Reviewer ${accepted.length + 1}`, hints: "\u2191\u2193 row \u00B7 \u2190\u2192 column \u00B7 Enter next \u00B7 Ctrl+D confirm", onBack: () => {
|
|
48
|
+
const last = accepted.at(-1);
|
|
49
|
+
if (last === undefined) {
|
|
50
|
+
onBack?.();
|
|
55
51
|
}
|
|
56
52
|
else {
|
|
57
|
-
|
|
53
|
+
updateSelection({ accepted: accepted.slice(0, -1), draft: last });
|
|
58
54
|
}
|
|
59
|
-
}, children: [
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
else {
|
|
64
|
-
onSubmit(reviewers);
|
|
65
|
-
}
|
|
66
|
-
} }, editing)] }));
|
|
55
|
+
}, children: [accepted.length > 0 && (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(ReviewerList, { reviewers: accepted, dimmed: true }) })), isAtLimit && (_jsxs(Text, { dimColor: true, children: ["Maximum ", maxReviewers, " reviewers. Press Ctrl+D to confirm them or Esc to edit the last one."] })), !isAtLimit && draft === null && _jsx(Text, { color: theme.failure, children: "No model is available for reviewers." }), draft !== null && (_jsx(ReviewerEditor, { models: models, reviewer: draft, onChange: (next) => updateSelection({ accepted, draft: next }), onSubmit: () => {
|
|
56
|
+
const nextAccepted = [...accepted, draft];
|
|
57
|
+
updateSelection({ accepted: nextAccepted, draft: nextDraft(models, nextAccepted) });
|
|
58
|
+
} }, accepted.length))] }));
|
|
67
59
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { basename } from 'node:path';
|
|
2
3
|
import { Box, Text } from 'ink';
|
|
3
4
|
import { useEffect, useState } from 'react';
|
|
4
5
|
import { waitingProgress } from '../../execution/agent-progress.js';
|
|
@@ -8,6 +9,7 @@ import { councilAgentId, numberReviewers, reviewerAgentId, runReview } from '../
|
|
|
8
9
|
import { describeReviewResult } from '../../review/review-output.js';
|
|
9
10
|
import { ReportResult } from '../components/report-result.js';
|
|
10
11
|
import { RunProgress } from '../components/run-progress.js';
|
|
12
|
+
import { describeScopeRange, fileNoun } from '../describe-scope.js';
|
|
11
13
|
const agentColumns = (configuration) => ({
|
|
12
14
|
provider: providerDisplayNames[configuration.provider],
|
|
13
15
|
model: configuration.modelLabel,
|
|
@@ -52,7 +54,6 @@ export const RunScreen = ({ session, dependencies, writeClipboard, openReport, o
|
|
|
52
54
|
runReview(session, dependencies, (agentId, change) => setRows((previous) => previous.map((row) => (row.id === agentId ? { ...row, progress: change(row.progress) } : row))), controller.signal).then((result) => setOutcome({ kind: 'finished', result }), (error) => setOutcome({ kind: 'failed', error: error instanceof Error ? error.message : String(error) }));
|
|
53
55
|
return () => controller.abort();
|
|
54
56
|
}, []);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
: `Reviewed ${session.scope.changedFiles.length} file(s)` }), _jsx(Box, { marginTop: 1, children: _jsx(RunProgress, { rows: rows }) }), outcome === null && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Ctrl+C cancels all agents" }) })), outcome !== null && report !== null && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ReportResult, { report: report, writeClipboard: writeClipboard, openReport: openReport, onQuit: onQuit }) }))] }));
|
|
57
|
+
const changedCount = session.scope.changedFiles.length;
|
|
58
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: [outcome === null ? 'Reviewing' : 'Reviewed', " ", changedCount, " ", fileNoun(changedCount), " \u00B7", ' ', basename(session.repository.root), " \u00B7 ", describeScopeRange(session.scope)] }), _jsx(Box, { marginTop: 1, children: _jsx(RunProgress, { rows: rows }) }), outcome === null && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Ctrl+C cancels all agents" }) })), outcome !== null && report !== null && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ReportResult, { report: report, writeClipboard: writeClipboard, openReport: openReport, onQuit: onQuit }) }))] }));
|
|
58
59
|
};
|
package/docs/council/council.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
You verify independent reviews against the supplied change and produce one trustworthy, actionable report. Judge evidence, not reviewer agreement.
|
|
4
4
|
|
|
5
|
-
- Verify every claim, merge duplicates and resolve contradictions. Separate confirmed findings, rejected claims with
|
|
5
|
+
- Verify every claim, merge duplicates and resolve contradictions. Separate confirmed findings, rejected claims with counterevidence and unresolved questions with missing evidence.
|
|
6
6
|
- Preserve evidence, repository-relative locations, attribution, smallest fixes and coverage gaps. Include severity and confidence; prioritize by impact. Mark incidental new findings as council.
|
|
7
7
|
- Return only a self-contained Markdown report in the session language. State when no issues were confirmed.
|
|
8
8
|
- Use level-3 headings only for confirmed findings, most severe first; the application counts them and lists them in its preview.
|
|
9
9
|
|
|
10
10
|
## Report opening
|
|
11
11
|
|
|
12
|
-
The application previews the title, the first paragraph and the finding headings. Open the report like this, translated, without an enclosing code fence:
|
|
12
|
+
The application previews the title, the first paragraph and the finding headings. Include any failed reviews and material coverage gaps in the first paragraph. Open the report like this, translated, without an enclosing code fence:
|
|
13
13
|
|
|
14
14
|
```markdown
|
|
15
15
|
# <one-line verdict>
|
package/package.json
CHANGED