reis-mobile 0.7.3 → 0.9.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +39 -0
- package/README.md +100 -26
- package/agents/android-architect.md +3 -3
- package/agents/android-performance-engineer.md +1 -1
- package/agents/flutter-architect.md +3 -3
- package/agents/flutter-performance-engineer.md +1 -1
- package/agents/ios-architect.md +3 -3
- package/agents/ios-performance-engineer.md +1 -1
- package/agents/kmp-architect.md +3 -3
- package/agents/kmp-performance-engineer.md +1 -1
- package/agents/lead-mobile.md +3 -0
- package/agents/mobile-release-engineer.md +3 -3
- package/agents/mobile-staff-engineer.md +1 -1
- package/agents/rn-architect.md +3 -3
- package/agents/rn-performance-engineer.md +1 -1
- package/bin/reis-mobile.mjs +123 -1
- package/commands/consult.md +95 -0
- package/commands/debate.md +15 -11
- package/commands/reis-mobile.md +7 -2
- package/commands/review.md +2 -0
- package/core/config/project.mjs +1 -1
- package/core/diagnostics/doctor.mjs +23 -1
- package/core/providers/catalog.mjs +168 -0
- package/core/providers/consult.mjs +220 -0
- package/core/providers/providers.mjs +156 -0
- package/core/router/intents.mjs +15 -0
- package/package.json +1 -1
- package/skills/mobile-accessibility/SKILL.md +3 -0
- package/skills/mobile-accessibility/references/localization.md +77 -0
- package/skills/mobile-observability/SKILL.md +137 -0
- package/skills/mobile-observability/references/android.md +105 -0
- package/skills/mobile-observability/references/flutter.md +86 -0
- package/skills/mobile-observability/references/ios.md +88 -0
- package/skills/mobile-observability/references/kotlin-multiplatform.md +76 -0
- package/skills/mobile-observability/references/react-native.md +62 -0
- package/skills/mobile-offline-sync/SKILL.md +132 -0
- package/skills/mobile-offline-sync/references/android.md +123 -0
- package/skills/mobile-offline-sync/references/flutter.md +103 -0
- package/skills/mobile-offline-sync/references/ios.md +103 -0
- package/skills/mobile-offline-sync/references/kotlin-multiplatform.md +114 -0
- package/skills/mobile-offline-sync/references/react-native.md +108 -0
- package/skills/mobile-performance/SKILL.md +88 -0
- package/skills/mobile-performance/references/android.md +96 -0
- package/skills/mobile-performance/references/background.md +87 -0
- package/skills/mobile-performance/references/flutter.md +101 -0
- package/skills/mobile-performance/references/ios.md +82 -0
- package/skills/mobile-performance/references/kotlin-multiplatform.md +62 -0
- package/skills/mobile-performance/references/react-native.md +85 -0
- package/skills/mobile-release/SKILL.md +2 -1
- package/skills/mobile-release/references/billing.md +92 -0
- package/skills/mobile-security/SKILL.md +3 -0
- package/skills/mobile-security/references/auth.md +92 -0
package/bin/reis-mobile.mjs
CHANGED
|
@@ -10,6 +10,8 @@ import { detectStack } from '../core/detection/stack-detector.mjs';
|
|
|
10
10
|
import { runDoctor } from '../core/diagnostics/doctor.mjs';
|
|
11
11
|
import { runInit } from '../core/install/init.mjs';
|
|
12
12
|
import { PLUGIN_ROOT } from '../core/paths.mjs';
|
|
13
|
+
import { DEFAULT_TIMEOUT_MS, consult, listProviderModels } from '../core/providers/consult.mjs';
|
|
14
|
+
import { parseProviderList, providerAllowlist, resolveProviders, selectProviders } from '../core/providers/providers.mjs';
|
|
13
15
|
import { loadRegistry, validateRegistry } from '../core/registry/registry.mjs';
|
|
14
16
|
import { releaseContext } from '../core/release/context.mjs';
|
|
15
17
|
import { INTENT_IDS } from '../core/router/intents.mjs';
|
|
@@ -30,6 +32,8 @@ Commands:
|
|
|
30
32
|
debug [problem...] Route a build or runtime failure and report the toolchain it runs on
|
|
31
33
|
test [request...] Prepare test routing, git context and toolchain checks (does not run tests)
|
|
32
34
|
release [request...] Prepare release evidence context (does not build or publish)
|
|
35
|
+
providers List the external model providers and what each one needs
|
|
36
|
+
consult [question...] Ask the configured external providers the same question
|
|
33
37
|
agents | skills | stacks List registered components
|
|
34
38
|
validate Validate the plugin's agents, skills and stacks
|
|
35
39
|
update-check Check npm for a newer reis-mobile release
|
|
@@ -44,6 +48,11 @@ Options:
|
|
|
44
48
|
--scope <scope> init: Claude Code scope, user (default), project or local
|
|
45
49
|
--uninstall init: remove the plugin and its marketplace from Claude Code and Codex, and the saved language
|
|
46
50
|
--force update-check: ignore the cached registry result
|
|
51
|
+
--providers <ids> providers/consult: comma-separated provider ids (default: every usable one)
|
|
52
|
+
--model <id> consult: model for the selected HTTP providers, overriding the configuration
|
|
53
|
+
--models providers: ask each provider which models it serves (network request)
|
|
54
|
+
--file <path> consult: read the question from a file instead of the arguments
|
|
55
|
+
--timeout <ms> consult: give up on a provider after this long (default 120000)
|
|
47
56
|
--lang <en|pt> Override the saved language for this run
|
|
48
57
|
--json Machine-readable output
|
|
49
58
|
-v, --version Print the version
|
|
@@ -59,6 +68,11 @@ const OPTIONS = {
|
|
|
59
68
|
scope: { type: 'string' },
|
|
60
69
|
uninstall: { type: 'boolean' },
|
|
61
70
|
force: { type: 'boolean' },
|
|
71
|
+
providers: { type: 'string' },
|
|
72
|
+
model: { type: 'string' },
|
|
73
|
+
models: { type: 'boolean' },
|
|
74
|
+
file: { type: 'string' },
|
|
75
|
+
timeout: { type: 'string' },
|
|
62
76
|
lang: { type: 'string' },
|
|
63
77
|
json: { type: 'boolean' },
|
|
64
78
|
version: { type: 'boolean', short: 'v' },
|
|
@@ -75,6 +89,8 @@ const COMMANDS = {
|
|
|
75
89
|
debug: commandDebug,
|
|
76
90
|
test: commandTest,
|
|
77
91
|
release: commandRelease,
|
|
92
|
+
providers: commandProviders,
|
|
93
|
+
consult: commandConsult,
|
|
78
94
|
agents: (options) => commandList('agents', options),
|
|
79
95
|
skills: (options) => commandList('skills', options),
|
|
80
96
|
stacks: (options) => commandList('stacks', options),
|
|
@@ -109,7 +125,7 @@ async function main(argv) {
|
|
|
109
125
|
return (await command({ ...options, dir: project.projectDir, project, prompt: rest.join(' ') })) ?? 0;
|
|
110
126
|
}
|
|
111
127
|
|
|
112
|
-
const PROJECT_COMMANDS = new Set(['detect', 'doctor', 'route', 'review', 'debug', 'test', 'release']);
|
|
128
|
+
const PROJECT_COMMANDS = new Set(['detect', 'doctor', 'route', 'review', 'debug', 'test', 'release', 'providers', 'consult']);
|
|
113
129
|
|
|
114
130
|
async function commandUpdateCheck({ force, json }) {
|
|
115
131
|
const result = await checkForUpdate({ force });
|
|
@@ -300,6 +316,112 @@ async function commandRelease({ dir, project, prompt, json, lang }) {
|
|
|
300
316
|
return 0;
|
|
301
317
|
}
|
|
302
318
|
|
|
319
|
+
/**
|
|
320
|
+
* External providers and what each one still needs. No network call: an executable on PATH
|
|
321
|
+
* or a credential in the environment is what decides `ready`, so `providers` and `doctor`
|
|
322
|
+
* stay offline. `--models` is the one exception, and asks the provider itself rather than
|
|
323
|
+
* shipping a model id that goes stale.
|
|
324
|
+
*/
|
|
325
|
+
async function commandProviders({ project, all, json, models, providers: requested, model, timeout }) {
|
|
326
|
+
const allow = providerAllowlist(project.config);
|
|
327
|
+
const resolution = await resolveProviders({ allow, model });
|
|
328
|
+
const requestedIds = parseProviderList(requested);
|
|
329
|
+
for (const warning of resolution.warnings) console.error(`! ${warning}`);
|
|
330
|
+
|
|
331
|
+
if (models) {
|
|
332
|
+
const selected = selectProviders(resolution.providers, requestedIds, { requireModel: false });
|
|
333
|
+
const catalogs = await listProviderModels({ providers: selected, timeoutMs: parseTimeout(timeout, 30_000) });
|
|
334
|
+
if (json) return print({ models: catalogs, configPath: resolution.configPath });
|
|
335
|
+
for (const catalog of catalogs) {
|
|
336
|
+
const summary = catalog.status === 'ok' ? `${catalog.models.length} model(s)` : `${catalog.status} · ${catalog.error}`;
|
|
337
|
+
console.log(`${catalog.provider.padEnd(14)}${summary}`);
|
|
338
|
+
for (const id of catalog.models) console.log(` · ${id}`);
|
|
339
|
+
}
|
|
340
|
+
return catalogs.some((catalog) => catalog.status === 'ok') ? 0 : 1;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (json) return print({ providers: resolution.providers, configPath: resolution.configPath, projectConfig: project.config?.path ?? null });
|
|
344
|
+
|
|
345
|
+
printProject(project);
|
|
346
|
+
const shown = all ? resolution.providers : resolution.providers.filter((provider) => provider.selectable);
|
|
347
|
+
for (const provider of shown) {
|
|
348
|
+
const model = provider.model ? `model ${provider.model} (${provider.modelSource})` : provider.kind === 'cli' ? 'own model' : 'no model';
|
|
349
|
+
console.log(`${provider.id.padEnd(14)}${PROVIDER_SYMBOLS[provider.status]} ${provider.status.padEnd(9)}${model.padEnd(34)}${provider.detail}`);
|
|
350
|
+
}
|
|
351
|
+
const usable = resolution.providers.filter((provider) => provider.runnable);
|
|
352
|
+
console.log(
|
|
353
|
+
usable.length
|
|
354
|
+
? `\n${usable.length} provider(s) ready: reis-mobile consult --providers ${usable.map((provider) => provider.id).join(',')} -- "<question>"`
|
|
355
|
+
: '\nNo provider is ready. Install the Codex or Gemini CLI, or set an API key and a model (reis-mobile providers --all).',
|
|
356
|
+
);
|
|
357
|
+
console.log(`Config ${resolution.configPath}`);
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const PROVIDER_SYMBOLS = { ready: '✓', local: '-', missing: '-', disabled: '-', blocked: '✗' };
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Asks the selected providers the same question. The core performs the call so no API key
|
|
365
|
+
* reaches the conversation, and both the question and every answer go through the redaction
|
|
366
|
+
* layer on the way through.
|
|
367
|
+
*/
|
|
368
|
+
async function commandConsult({ dir, project, json, providers: requested, model, timeout, file, prompt, lang }) {
|
|
369
|
+
const question = file ? await readFile(file, 'utf8') : prompt || (await readStdin());
|
|
370
|
+
if (!question.trim()) {
|
|
371
|
+
console.error('consult needs a question. Pass it as an argument, with --file <path>, or on stdin.');
|
|
372
|
+
return 1;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const allow = providerAllowlist(project.config);
|
|
376
|
+
const resolution = await resolveProviders({ allow, model });
|
|
377
|
+
for (const warning of resolution.warnings) console.error(`! ${warning}`);
|
|
378
|
+
const selected = selectProviders(resolution.providers, parseProviderList(requested));
|
|
379
|
+
if (!selected.length) {
|
|
380
|
+
console.error('No external provider is ready. Run `reis-mobile providers --all` to see what each one needs.');
|
|
381
|
+
return 1;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const detection = await detectStack(dir);
|
|
385
|
+
const language = await resolveLanguage({ flag: lang });
|
|
386
|
+
const answers = await consult({ prompt: question, providers: selected, timeoutMs: parseTimeout(timeout, DEFAULT_TIMEOUT_MS) });
|
|
387
|
+
const answered = answers.results.filter((result) => result.status === 'ok');
|
|
388
|
+
|
|
389
|
+
if (json) {
|
|
390
|
+
print({ stack: detection.stack, language, projectConfig: project.config?.path ?? null, ...answers });
|
|
391
|
+
return answered.length ? 0 : 1;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
printProject(project);
|
|
395
|
+
console.log(`Stack ${detection.stack}`);
|
|
396
|
+
printLanguage(language);
|
|
397
|
+
console.log(`Question ${answers.prompt.chars} chars${answers.prompt.redacted ? ' (secrets masked)' : ''}`);
|
|
398
|
+
for (const result of answers.results) {
|
|
399
|
+
const heading = `${result.provider}${result.model ? ` · ${result.model}` : ''} · ${result.status} · ${(result.durationMs / 1000).toFixed(1)}s`;
|
|
400
|
+
console.log(`\n--- ${heading} ---`);
|
|
401
|
+
console.log(result.status === 'ok' ? result.text : `! ${result.error}`);
|
|
402
|
+
}
|
|
403
|
+
console.log(
|
|
404
|
+
answered.length
|
|
405
|
+
? '\nExternal answers are an outside opinion: they do not know this project. Verify every claim in the code.'
|
|
406
|
+
: '\nNo provider answered.',
|
|
407
|
+
);
|
|
408
|
+
return answered.length ? 0 : 1;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function parseTimeout(value, fallback) {
|
|
412
|
+
if (value === undefined) return fallback;
|
|
413
|
+
const timeout = Number(value);
|
|
414
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new Error(`--timeout must be a positive number of milliseconds, got ${JSON.stringify(value)}`);
|
|
415
|
+
return timeout;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function readStdin() {
|
|
419
|
+
if (process.stdin.isTTY) return '';
|
|
420
|
+
const chunks = [];
|
|
421
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
422
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
423
|
+
}
|
|
424
|
+
|
|
303
425
|
async function commandList(kind, { json }) {
|
|
304
426
|
const registry = await loadRegistry();
|
|
305
427
|
const items = registry[kind];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Second opinion from external providers — builds a stack-aware briefing, sends it through the provider layer and reconciles every outside claim against the code
|
|
3
|
+
argument-hint: "[--providers a,b] [--model <id>] [--files a,b] <question>"
|
|
4
|
+
allowed-tools: ["Bash(node:*)", "Read", "Grep", "Glob", "Write"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# reis-mobile consult
|
|
8
|
+
|
|
9
|
+
Arguments received: `$ARGUMENTS`
|
|
10
|
+
|
|
11
|
+
Use this command when a mobile decision or finding deserves a model that does not share this session's blind spots: a migration you are unsure about, a diagnosis you cannot confirm, a trade-off where you suspect you are anchored. For a question the code answers on its own, answer it directly and say why a consult was not worth it.
|
|
12
|
+
|
|
13
|
+
**This sends project context to a third party.** Everything in the briefing leaves the session, including the excerpts of the files you include. Name the providers that will receive it before sending, and never include a credential, a store key or a `.env` value.
|
|
14
|
+
|
|
15
|
+
## 1. Parse the arguments
|
|
16
|
+
|
|
17
|
+
| Flag | Default | Effect |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `--providers a,b` | every ready provider | Explicit provider ids, from step 2. |
|
|
20
|
+
| `--model <id>` | per-provider configuration | Model for the selected HTTP providers. |
|
|
21
|
+
| `--files a,b` | files named in the question | Extra files to include in the briefing. |
|
|
22
|
+
| `--timeout <ms>` | 120000 | How long to wait for each provider. |
|
|
23
|
+
|
|
24
|
+
Whatever remains is the question, as free text. Without a question, explain the usage and stop.
|
|
25
|
+
|
|
26
|
+
## 2. Check who can answer
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/reis-mobile.mjs" providers --dir "$PWD" --all
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`ready` providers can be used now. `missing` ones state the executable or the credential they need, `local` needs a server running, `disabled` is off in the user config and `blocked` is not in the `providers:` allowlist of `.reis-mobile/config.yaml` — a team decision, which you do not work around. With no ready provider, show what each one needs and stop.
|
|
33
|
+
|
|
34
|
+
## 3. Route the question and gather the evidence
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/reis-mobile.mjs" route --dir "$PWD" -- "<question>"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Use the `Language` line for everything the user reads (`en` English, `pt` Brazilian Portuguese; `-` means the language of the request). Read the selected agent and skills as `/reis-mobile:review` does, then resolve the paths in the question and in `--files` and read them. External providers receive content, never paths: they cannot open your repository.
|
|
41
|
+
|
|
42
|
+
**Form your own position first.** Write it down before sending anything. A consult that starts without one turns into agreement with whatever comes back.
|
|
43
|
+
|
|
44
|
+
## 4. Write the briefing
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
mkdir -p ".reis-mobile/consults/<NNN-question-slug>"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`NNN` is sequential within `.reis-mobile/consults/`. Write `briefing.md` in that folder with the Write tool, self-contained:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
QUESTION: <question>
|
|
54
|
+
STACK: <detected stack, variant and platforms>
|
|
55
|
+
LANGUAGE: <language — answer in it>
|
|
56
|
+
CODE: <the relevant excerpts, with file:line for each>
|
|
57
|
+
CONSTRAINTS: <versions, SDK targets, deadlines or rules that limit the answer>
|
|
58
|
+
|
|
59
|
+
You do not have access to this repository: everything you need is above.
|
|
60
|
+
Answer in at most 500 words: your recommendation, the evidence in the excerpts that
|
|
61
|
+
supports it, what it costs, and what would change your mind. Say explicitly when the
|
|
62
|
+
excerpts are not enough to decide.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 5. Consult
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/reis-mobile.mjs" consult --dir "$PWD" \
|
|
69
|
+
--file ".reis-mobile/consults/<id>/briefing.md" \
|
|
70
|
+
[--providers <ids>] [--model <id>] [--timeout <ms>]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The CLI performs the call, so no API key passes through this conversation; the question is masked by the redaction layer on the way out and every answer on the way in. Each block is headed by the provider, its model, its status and its duration. A `no-model`, `timeout`, `unavailable` or `error` status is information to report, not a failure to retry blindly — retry once at most, and only for `timeout`.
|
|
74
|
+
|
|
75
|
+
## 6. Reconcile against the code
|
|
76
|
+
|
|
77
|
+
External answers do not know this project and have no reis-mobile skills. For every claim that matters:
|
|
78
|
+
|
|
79
|
+
- confirm it in the code and cite `file:line`;
|
|
80
|
+
- mark as **unverifiable** what the excerpts cannot settle;
|
|
81
|
+
- mark as **wrong** what the code contradicts, with the evidence.
|
|
82
|
+
|
|
83
|
+
A claim that only repeats the briefing back adds nothing. Do not adopt a recommendation because several providers agree: they may share a training bias that your code does not.
|
|
84
|
+
|
|
85
|
+
## 7. Report
|
|
86
|
+
|
|
87
|
+
In this order:
|
|
88
|
+
|
|
89
|
+
1. **Decision** — your recommendation, with the reason, in one paragraph.
|
|
90
|
+
2. **Where the providers agreed with you** and where they did not, naming each one.
|
|
91
|
+
3. **Confirmed claims** with `file:line`, then **unverifiable** and **wrong** ones.
|
|
92
|
+
4. **Action plan**, if the decision implies work.
|
|
93
|
+
5. The briefing path and the providers that received it.
|
|
94
|
+
|
|
95
|
+
Do not apply any code change: a consult ends in a recommendation. Implementing it is a new request.
|
package/commands/debate.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Structured debate between the mobile specialists — each agent defends its perspective, rebuts the others, and lead-mobile decides
|
|
3
|
-
argument-hint: "[--rounds N] [--agents a,b,c] [--external] <question>"
|
|
4
|
-
allowed-tools: ["Bash(node:*)", "Bash(
|
|
3
|
+
argument-hint: "[--rounds N] [--agents a,b,c] [--external] [--providers a,b] <question>"
|
|
4
|
+
allowed-tools: ["Bash(node:*)", "Bash(mkdir:*)", "Agent", "Read", "Write", "Grep", "Glob"]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# reis-mobile debate
|
|
@@ -16,7 +16,8 @@ Use this command for architecture decisions with real trade-offs, not for routin
|
|
|
16
16
|
|---|---|---|
|
|
17
17
|
| `--rounds N` | 2 | Number of rounds, from 1 to 3. Above 3, warn and use 3. |
|
|
18
18
|
| `--agents a,b,c` | automatic | Explicit participants, using the names from the table in step 3. |
|
|
19
|
-
| `--external` | off | Adds
|
|
19
|
+
| `--external` | off | Adds every ready external provider to the debate. |
|
|
20
|
+
| `--providers a,b` | every ready one | Explicit provider ids; implies `--external`. |
|
|
20
21
|
|
|
21
22
|
Whatever remains is the question, as free text. Without a question, explain the usage and stop.
|
|
22
23
|
|
|
@@ -98,23 +99,26 @@ Ground every claim in file:line. Do not write what you have not verified.
|
|
|
98
99
|
Write your position to: .reis-mobile/debates/<id>/rounds/r1_<agent>.md
|
|
99
100
|
```
|
|
100
101
|
|
|
101
|
-
### 5.1 External providers (only with `--external`)
|
|
102
|
+
### 5.1 External providers (only with `--external` or `--providers`)
|
|
102
103
|
|
|
103
|
-
|
|
104
|
+
Ask the core which providers can answer, instead of probing for executables yourself:
|
|
104
105
|
|
|
105
106
|
```bash
|
|
106
|
-
|
|
107
|
-
command -v gemini >/dev/null 2>&1 && echo gemini
|
|
107
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/reis-mobile.mjs" providers --dir "$PWD" --all
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
`ready` providers can be used now; `missing`, `local`, `disabled` and `blocked` state what each one needs, and `blocked` means the repository's `providers:` allowlist excludes it — a team decision you do not work around. With no ready provider, carry on with the agents alone: their absence is informative, not an error. State at the opening who joined and who was missing.
|
|
111
|
+
|
|
112
|
+
Write the same briefing you gave the agents to `rounds/r<N>_briefing.md` with the Write tool — a file, so nothing depends on shell quoting — and send it:
|
|
111
113
|
|
|
112
114
|
```bash
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/reis-mobile.mjs" consult --dir "$PWD" \
|
|
116
|
+
--file ".reis-mobile/debates/<id>/rounds/r<N>_briefing.md" [--providers <ids>]
|
|
115
117
|
```
|
|
116
118
|
|
|
117
|
-
|
|
119
|
+
The CLI performs the call, so no API key passes through this conversation, and the briefing is masked by the redaction layer before it leaves the process. Write each answer to `rounds/r<N>_<provider>.md` with the Write tool, keeping the provider id and the status from the output header. Treat a `timeout`, `error` or `no-model` status as a participant that did not answer this round.
|
|
120
|
+
|
|
121
|
+
They do not have the plugin skills and do not know the project: the briefing carries the entire context, and the output is an outside opinion to be checked against the code before it enters the synthesis. In round 2 they receive the others' positions exactly as the agents do.
|
|
118
122
|
|
|
119
123
|
## 6. Round 2 — rebuttal
|
|
120
124
|
|
package/commands/reis-mobile.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: reis-mobile entry point — lists the commands or forwards to doctor, project, review, debug, test, release, debate or the right agent from a free-form request
|
|
3
|
-
argument-hint: "[doctor | project | review | debug | test | release | debate | free-form request]"
|
|
2
|
+
description: reis-mobile entry point — lists the commands or forwards to doctor, project, review, debug, test, release, debate, consult or the right agent from a free-form request
|
|
3
|
+
argument-hint: "[doctor | project | review | debug | test | release | debate | consult | free-form request]"
|
|
4
4
|
allowed-tools: ["Bash(node:*)", "Bash(git:*)", "Read", "Grep", "Glob", "Edit", "Write"]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -26,6 +26,7 @@ Reply only with this list. The only thing to run is `node "${CLAUDE_PLUGIN_ROOT}
|
|
|
26
26
|
/reis-mobile:test [request] Runs, writes, audits or fixes tests using the project's test setup
|
|
27
27
|
/reis-mobile:release [request] Audits store readiness with evidence and a go/no-go verdict
|
|
28
28
|
/reis-mobile:debate <question> Debate between the specialists, with a decision by lead-mobile
|
|
29
|
+
/reis-mobile:consult <question> Second opinion from the configured external providers, checked against the code
|
|
29
30
|
/reis-mobile <free-form request> Forwards the request to the agent and skills for the detected stack
|
|
30
31
|
```
|
|
31
32
|
|
|
@@ -52,6 +53,10 @@ Read `${CLAUDE_PLUGIN_ROOT}/commands/debug.md` and follow all of it, treating th
|
|
|
52
53
|
|
|
53
54
|
Read `${CLAUDE_PLUGIN_ROOT}/commands/debate.md` and follow all of it, treating the rest of the arguments (without the word `debate`) as `$ARGUMENTS`.
|
|
54
55
|
|
|
56
|
+
## `consult [options] <question>`
|
|
57
|
+
|
|
58
|
+
Read `${CLAUDE_PLUGIN_ROOT}/commands/consult.md` and follow all of it, treating the rest of the arguments (without the word `consult`) as `$ARGUMENTS`. This entry point has the same behavior as `/reis-mobile:consult`.
|
|
59
|
+
|
|
55
60
|
## `test [request]`
|
|
56
61
|
|
|
57
62
|
Read `${CLAUDE_PLUGIN_ROOT}/commands/test.md` and follow all of it, treating the rest of the arguments (without the word `test`) as `$ARGUMENTS`. This entry point has the same behavior as `/reis-mobile:test`.
|
package/commands/review.md
CHANGED
|
@@ -51,3 +51,5 @@ Confirm every finding in the code before reporting it. Do not modify files: this
|
|
|
51
51
|
Answer in the format defined by the agent (Summary, Critical, Bugs, Architecture, Security, Performance, Maintainability, Suggested changes), translating the headings when the language is not English. On the first line of the Summary, state the stack, the agent and the skills used, so the routing is visible.
|
|
52
52
|
|
|
53
53
|
If the router emitted warnings (lines starting with `!`), mention them in the Summary.
|
|
54
|
+
|
|
55
|
+
When the user wants a finding cross-checked by a model outside this session, point them at `/reis-mobile:consult <question>`: it builds the briefing, sends it to the configured providers and reconciles every outside claim against the code. A review never sends project context to a third party on its own.
|
package/core/config/project.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { parseYamlSubset } from '../registry/frontmatter.mjs';
|
|
|
5
5
|
|
|
6
6
|
export const PROJECT_CONFIG = join('.reis-mobile', 'config.yaml');
|
|
7
7
|
|
|
8
|
-
const KNOWN_KEYS = new Set(['app']);
|
|
8
|
+
const KNOWN_KEYS = new Set(['app', 'providers']);
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Finds the nearest `.reis-mobile/config.yaml`, starting at `dir` and walking up, so a
|
|
@@ -4,8 +4,10 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
|
|
7
|
+
import { findProjectConfig } from '../config/project.mjs';
|
|
7
8
|
import { createProbe } from '../detection/project-probe.mjs';
|
|
8
9
|
import { detectStack } from '../detection/stack-detector.mjs';
|
|
10
|
+
import { providerAllowlist, resolveProviders } from '../providers/providers.mjs';
|
|
9
11
|
|
|
10
12
|
const run = promisify(execFile);
|
|
11
13
|
const TOOL_TIMEOUT_MS = 30_000;
|
|
@@ -47,8 +49,9 @@ export async function runDoctor({ projectDir = process.cwd(), all = false, platf
|
|
|
47
49
|
const tools = TOOLS.filter((tool) => all || tool.relevant(context));
|
|
48
50
|
const toolChecks = await Promise.all(tools.map((tool) => checkTool(tool, platform)));
|
|
49
51
|
const projectChecks = await checkProject(probe, detection, files);
|
|
52
|
+
const providerChecks = await checkProviders(detection.projectDir, all);
|
|
50
53
|
|
|
51
|
-
const checks = [...toolChecks, ...projectChecks];
|
|
54
|
+
const checks = [...toolChecks, ...projectChecks, ...providerChecks];
|
|
52
55
|
const warnings = checks.filter((check) => check.status === 'warning').length;
|
|
53
56
|
|
|
54
57
|
return {
|
|
@@ -85,6 +88,25 @@ async function checkTool(tool, platform) {
|
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
/**
|
|
92
|
+
* External model providers, resolved from PATH and the environment only: `doctor` makes no
|
|
93
|
+
* network call, so a reachable provider is never confirmed here, just a configured one.
|
|
94
|
+
*/
|
|
95
|
+
async function checkProviders(projectDir, all) {
|
|
96
|
+
const { providers } = await resolveProviders({ allow: providerAllowlist(findProjectConfig(projectDir)) });
|
|
97
|
+
const shown = all ? providers : providers.filter((provider) => provider.status === 'ready');
|
|
98
|
+
if (!shown.length) {
|
|
99
|
+
return [{ id: 'providers', label: 'Providers', section: 'integrations', status: 'info', detail: 'none configured (reis-mobile providers --all)' }];
|
|
100
|
+
}
|
|
101
|
+
return shown.map((provider) => ({
|
|
102
|
+
id: `provider-${provider.id}`,
|
|
103
|
+
label: provider.label,
|
|
104
|
+
section: 'integrations',
|
|
105
|
+
status: provider.status === 'ready' ? 'ok' : 'info',
|
|
106
|
+
detail: `${provider.status} · ${provider.model ? `model ${provider.model}` : provider.kind === 'cli' ? 'own model' : 'no model'}`,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
88
110
|
async function checkAndroidSdk() {
|
|
89
111
|
const candidates = [
|
|
90
112
|
process.env.ANDROID_HOME,
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// External model providers reis-mobile can ask for a second opinion.
|
|
2
|
+
//
|
|
3
|
+
// A provider is data, not code paths spread across commands: the catalog says how to reach
|
|
4
|
+
// it, which credential proves it is usable and where its model catalog lives. Nothing here
|
|
5
|
+
// carries a default model id — a provider is the only authority on its own catalog, and a
|
|
6
|
+
// hardcoded id becomes wrong the day it is retired. `providers --models` asks instead.
|
|
7
|
+
|
|
8
|
+
const JSON_HEADERS = { 'content-type': 'application/json' };
|
|
9
|
+
|
|
10
|
+
/** OpenAI, OpenRouter and any other `/chat/completions` endpoint share this shape. */
|
|
11
|
+
function openAiCompatible({ id, label, docs, keys, modelEnv, baseEnv, defaultBase, headers = {} }) {
|
|
12
|
+
const base = (env) => trimTrailingSlash(env[baseEnv] || defaultBase);
|
|
13
|
+
const auth = (env) => ({ authorization: `Bearer ${credential(env, keys)}`, ...headers });
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
label,
|
|
17
|
+
kind: 'http',
|
|
18
|
+
docs,
|
|
19
|
+
keys,
|
|
20
|
+
modelEnv,
|
|
21
|
+
needsModel: true,
|
|
22
|
+
base,
|
|
23
|
+
chat: ({ prompt, model, env }) => ({
|
|
24
|
+
url: `${base(env)}/chat/completions`,
|
|
25
|
+
init: {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { ...JSON_HEADERS, ...auth(env) },
|
|
28
|
+
body: JSON.stringify({ model, messages: [{ role: 'user', content: prompt }] }),
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
readChat: (body) => body?.choices?.[0]?.message?.content ?? '',
|
|
32
|
+
catalog: ({ env }) => ({ url: `${base(env)}/models`, init: { headers: auth(env) } }),
|
|
33
|
+
readCatalog: (body) => (body?.data ?? []).map((item) => item?.id).filter(Boolean),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const PROVIDERS = [
|
|
38
|
+
{
|
|
39
|
+
id: 'codex',
|
|
40
|
+
label: 'Codex CLI',
|
|
41
|
+
kind: 'cli',
|
|
42
|
+
docs: 'https://developers.openai.com/codex/cli',
|
|
43
|
+
command: 'codex',
|
|
44
|
+
// `--sandbox read-only` because a second opinion never needs to write, and
|
|
45
|
+
// `--skip-git-repo-check` because the call runs in a temporary directory, not a repository.
|
|
46
|
+
// Verified against `codex exec --help` (codex-cli 0.155.1); the prompt is an argument.
|
|
47
|
+
argv: ({ prompt }) => ['exec', '--sandbox', 'read-only', '--skip-git-repo-check', prompt],
|
|
48
|
+
stdin: () => undefined,
|
|
49
|
+
version: ['--version'],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'gemini-cli',
|
|
53
|
+
label: 'Gemini CLI',
|
|
54
|
+
kind: 'cli',
|
|
55
|
+
docs: 'https://github.com/google-gemini/gemini-cli',
|
|
56
|
+
command: 'gemini',
|
|
57
|
+
// `-p ""` plus the briefing on stdin, as `--prompt` is appended to stdin input, and
|
|
58
|
+
// `--approval-mode plan`, the read-only mode (`gemini --help`, gemini-cli 0.34.0).
|
|
59
|
+
argv: () => ['-p', '', '-o', 'text', '--approval-mode', 'plan'],
|
|
60
|
+
stdin: ({ prompt }) => prompt,
|
|
61
|
+
version: ['--version'],
|
|
62
|
+
},
|
|
63
|
+
openAiCompatible({
|
|
64
|
+
id: 'openai',
|
|
65
|
+
label: 'OpenAI',
|
|
66
|
+
docs: 'https://platform.openai.com/docs/api-reference',
|
|
67
|
+
keys: ['OPENAI_API_KEY'],
|
|
68
|
+
modelEnv: 'OPENAI_MODEL',
|
|
69
|
+
baseEnv: 'OPENAI_BASE_URL',
|
|
70
|
+
defaultBase: 'https://api.openai.com/v1',
|
|
71
|
+
}),
|
|
72
|
+
openAiCompatible({
|
|
73
|
+
id: 'openrouter',
|
|
74
|
+
label: 'OpenRouter',
|
|
75
|
+
docs: 'https://openrouter.ai/docs',
|
|
76
|
+
keys: ['OPENROUTER_API_KEY'],
|
|
77
|
+
modelEnv: 'OPENROUTER_MODEL',
|
|
78
|
+
baseEnv: 'OPENROUTER_BASE_URL',
|
|
79
|
+
defaultBase: 'https://openrouter.ai/api/v1',
|
|
80
|
+
headers: { 'x-title': 'reis-mobile' },
|
|
81
|
+
}),
|
|
82
|
+
{
|
|
83
|
+
id: 'gemini',
|
|
84
|
+
label: 'Gemini API',
|
|
85
|
+
kind: 'http',
|
|
86
|
+
docs: 'https://ai.google.dev/gemini-api/docs',
|
|
87
|
+
keys: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
|
|
88
|
+
modelEnv: 'GEMINI_MODEL',
|
|
89
|
+
needsModel: true,
|
|
90
|
+
base: geminiBase,
|
|
91
|
+
chat: ({ prompt, model, env }) => ({
|
|
92
|
+
url: `${geminiBase(env)}/models/${encodeURIComponent(model)}:generateContent`,
|
|
93
|
+
init: {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: { ...JSON_HEADERS, 'x-goog-api-key': credential(env, ['GEMINI_API_KEY', 'GOOGLE_API_KEY']) },
|
|
96
|
+
body: JSON.stringify({ contents: [{ role: 'user', parts: [{ text: prompt }] }] }),
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
readChat: (body) =>
|
|
100
|
+
(body?.candidates?.[0]?.content?.parts ?? [])
|
|
101
|
+
.map((part) => part?.text)
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.join(''),
|
|
104
|
+
catalog: ({ env }) => ({
|
|
105
|
+
url: `${geminiBase(env)}/models`,
|
|
106
|
+
init: { headers: { 'x-goog-api-key': credential(env, ['GEMINI_API_KEY', 'GOOGLE_API_KEY']) } },
|
|
107
|
+
}),
|
|
108
|
+
readCatalog: (body) => (body?.models ?? []).map((item) => String(item?.name ?? '').replace(/^models\//, '')).filter(Boolean),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'ollama',
|
|
112
|
+
label: 'Ollama',
|
|
113
|
+
kind: 'http',
|
|
114
|
+
docs: 'https://github.com/ollama/ollama/blob/main/docs/api.md',
|
|
115
|
+
// No credential: a local server is either running or it is not, and only a request tells.
|
|
116
|
+
keys: [],
|
|
117
|
+
local: true,
|
|
118
|
+
modelEnv: 'OLLAMA_MODEL',
|
|
119
|
+
needsModel: true,
|
|
120
|
+
base: ollamaBase,
|
|
121
|
+
chat: ({ prompt, model, env }) => ({
|
|
122
|
+
url: `${ollamaBase(env)}/api/chat`,
|
|
123
|
+
init: {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: JSON_HEADERS,
|
|
126
|
+
body: JSON.stringify({ model, messages: [{ role: 'user', content: prompt }], stream: false }),
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
readChat: (body) => body?.message?.content ?? '',
|
|
130
|
+
catalog: ({ env }) => ({ url: `${ollamaBase(env)}/api/tags`, init: {} }),
|
|
131
|
+
readCatalog: (body) => (body?.models ?? []).map((item) => item?.name).filter(Boolean),
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
export const PROVIDER_IDS = PROVIDERS.map((provider) => provider.id);
|
|
136
|
+
|
|
137
|
+
export function findProvider(id) {
|
|
138
|
+
return PROVIDERS.find((provider) => provider.id === id) ?? null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function unknownProviderMessage(id) {
|
|
142
|
+
return `Unknown provider "${id}". Use one of: ${PROVIDER_IDS.join(', ')}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The endpoint a provider is reached at, so `providers` can show where a call would go. */
|
|
146
|
+
export function providerEndpoint(provider, env = process.env) {
|
|
147
|
+
return provider.kind === 'cli' ? provider.command : provider.base(env);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function geminiBase(env) {
|
|
151
|
+
return trimTrailingSlash(env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** `OLLAMA_HOST` is commonly set without a scheme (`127.0.0.1:11434`). */
|
|
155
|
+
function ollamaBase(env) {
|
|
156
|
+
const host = (env.OLLAMA_HOST || '').trim();
|
|
157
|
+
if (!host) return 'http://127.0.0.1:11434';
|
|
158
|
+
return trimTrailingSlash(/^https?:\/\//.test(host) ? host : `http://${host}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function credential(env, keys) {
|
|
162
|
+
for (const key of keys) if (env[key]) return env[key];
|
|
163
|
+
return '';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function trimTrailingSlash(value) {
|
|
167
|
+
return String(value).replace(/\/+$/, '');
|
|
168
|
+
}
|