@kyo-so/cli 0.2.0 → 0.3.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.ja.md CHANGED
@@ -20,6 +20,14 @@ Kyoso はコード変更を適用しません。
20
20
 
21
21
  グローバルインストールは不要です。Kyoso は `npx` または `bunx` で実行します。
22
22
 
23
+ ### Claude Only / Codex Only
24
+
25
+ Kyoso は Claude だけ、または Codex だけでも実行できます。利用できない backend は `kyoso.config.ts` で無効化してください。例は `examples/claude-only.config.ts` と `examples/codex-only.config.ts` にあります。
26
+
27
+ single-agent mode では、残った backend が `combined_reviewer` として 1 回だけ実行され、implementation と architecture/security の両方を確認します。JSON output には `reviewMode: "single_agent"` と `agentsUsed` が入り、Markdown output には cross-model verification が行われていないことと disagreements が N/A であることを表示します。
28
+
29
+ この mode では独立した cross-model validation はなく、自己レビュー bias が残ります。一方で、別プロセスの read-only review、temporary snapshots、adversarial review prompts、secret scanning、deterministic gates は利用できます。
30
+
23
31
  ### Claude Code
24
32
 
25
33
  1. Claude 認証を準備します。
package/README.md CHANGED
@@ -18,6 +18,14 @@ Kyoso does not apply code changes.
18
18
 
19
19
  No global install is required. Run Kyoso through `npx` or `bunx`.
20
20
 
21
+ ### Claude Only / Codex Only
22
+
23
+ Kyoso can run when only Claude or only Codex is available. Disable the missing backend in `kyoso.config.ts` using `examples/claude-only.config.ts` or `examples/codex-only.config.ts`.
24
+
25
+ In single-agent mode, the remaining backend runs once as `combined_reviewer` and covers both implementation and architecture/security focus areas. JSON output includes `reviewMode: "single_agent"` and `agentsUsed`; Markdown output states that cross-model verification was not performed and marks disagreements as N/A.
26
+
27
+ This mode does not provide independent cross-model validation and may retain self-review bias. It still provides a separate read-only review process, temporary snapshots, adversarial review prompts, secret scanning, and deterministic gates.
28
+
21
29
  ### Claude Code
22
30
 
23
31
  1. Prepare Claude authentication.
package/README.zh-CN.md CHANGED
@@ -20,6 +20,14 @@ Kyoso 不会应用代码更改。
20
20
 
21
21
  无需全局安装。通过 `npx` 或 `bunx` 运行 Kyoso。
22
22
 
23
+ ### Claude Only / Codex Only
24
+
25
+ Kyoso 可以在只有 Claude 或只有 Codex 可用时运行。请在 `kyoso.config.ts` 中禁用缺失的 backend;示例见 `examples/claude-only.config.ts` 和 `examples/codex-only.config.ts`。
26
+
27
+ 在 single-agent mode 中,剩余 backend 会以 `combined_reviewer` 运行一次,同时覆盖 implementation 和 architecture/security 两类关注点。JSON output 会包含 `reviewMode: "single_agent"` 和 `agentsUsed`;Markdown output 会说明未执行 cross-model verification,并将 disagreements 标记为 N/A。
28
+
29
+ 该 mode 不提供独立的 cross-model validation,仍可能有 self-review bias。它仍保留独立只读 review process、temporary snapshots、adversarial review prompts、secret scanning 和 deterministic gates。
30
+
23
31
  ### Claude Code
24
32
 
25
33
  1. 准备 Claude authentication。
@@ -1,2 +1,2 @@
1
- import type { KyosoReviewRequest, ReviewTool } from "../core/types.js";
2
- export declare function buildAgentPrompt(tool: ReviewTool, request: KyosoReviewRequest, agent: "codex" | "claude"): string;
1
+ import type { AgentName, AgentRole, KyosoReviewRequest, ReviewTool } from "../core/types.js";
2
+ export declare function buildAgentPrompt(tool: ReviewTool, request: KyosoReviewRequest, agent: AgentName, role: AgentRole): string;
package/dist/bin/kyoso.js CHANGED
@@ -169600,7 +169600,7 @@ function languageFromPath(path) {
169600
169600
 
169601
169601
  // src/cli/doctor.ts
169602
169602
  import { accessSync } from "node:fs";
169603
- import { delimiter } from "node:path";
169603
+ import { delimiter as delimiter2 } from "node:path";
169604
169604
 
169605
169605
  // src/config/loadConfig.ts
169606
169606
  import { access as access2, readFile as readFile3 } from "node:fs/promises";
@@ -184013,7 +184013,11 @@ var agentSchema = exports_external.object({
184013
184013
  command: exports_external.string(),
184014
184014
  args: exports_external.array(exports_external.string()).default([]),
184015
184015
  model: exports_external.string().optional(),
184016
- role: exports_external.enum(["implementation_reviewer", "architecture_security_reviewer"]),
184016
+ role: exports_external.enum([
184017
+ "implementation_reviewer",
184018
+ "architecture_security_reviewer",
184019
+ "combined_reviewer"
184020
+ ]),
184017
184021
  timeoutMs: exports_external.number().int().positive().default(120000),
184018
184022
  env: exports_external.record(exports_external.string(), exports_external.string()).default({}),
184019
184023
  auth: exports_external.object({
@@ -184293,7 +184297,7 @@ var REDACTION = "[KYOSO_REDACTED]";
184293
184297
  var DEFAULT_AGENT_TIMEOUT_MS = 120000;
184294
184298
  var RAW_OUTPUT_MAX_CHARS = 16384;
184295
184299
  var KYOSO_CHILD_AGENT = "KYOSO_CHILD_AGENT";
184296
- var KYOSO_VERSION = "0.1.0";
184300
+ var KYOSO_VERSION = "0.3.0";
184297
184301
 
184298
184302
  // src/security/sanitizeText.ts
184299
184303
  var SENSITIVE_TEXT_PATTERNS = [
@@ -184555,7 +184559,7 @@ import { spawnSync } from "node:child_process";
184555
184559
  import { existsSync, readFileSync } from "node:fs";
184556
184560
  import { cp, mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
184557
184561
  import { homedir as homedir2 } from "node:os";
184558
- import { dirname as dirname4, join as join2 } from "node:path";
184562
+ import { delimiter, dirname as dirname4, join as join2 } from "node:path";
184559
184563
  import { fileURLToPath } from "node:url";
184560
184564
  async function runSetup(options) {
184561
184565
  const client = parseClient(options.client);
@@ -184564,6 +184568,7 @@ async function runSetup(options) {
184564
184568
  const context = {
184565
184569
  cwd: options.cwd,
184566
184570
  home: options.env?.HOME ?? homedir2(),
184571
+ env: options.env ?? process.env,
184567
184572
  write: options.write,
184568
184573
  scope: options.global ? "global" : "project",
184569
184574
  mcpCommand: command,
@@ -184634,7 +184639,8 @@ async function setupCodex(context) {
184634
184639
  sourceDir: context.sourceSkillDir,
184635
184640
  destinationDir: skillDestination("codex", context.scope, context.cwd, context.home),
184636
184641
  write: context.write
184637
- })
184642
+ }),
184643
+ ...singleAgentAdvice(context, "codex")
184638
184644
  ];
184639
184645
  }
184640
184646
  async function setupClaudeCode(context) {
@@ -184645,7 +184651,8 @@ async function setupClaudeCode(context) {
184645
184651
  sourceDir: context.sourceSkillDir,
184646
184652
  destinationDir: skillDestination("claude-code", context.scope, context.cwd, context.home),
184647
184653
  write: context.write
184648
- })
184654
+ }),
184655
+ ...singleAgentAdvice(context, "claude-code")
184649
184656
  ];
184650
184657
  }
184651
184658
  async function ensureCodexMcp(context) {
@@ -184956,6 +184963,41 @@ function diffForJson(path, before, after) {
184956
184963
  function statusWord(value) {
184957
184964
  return value ? "ok" : "missing";
184958
184965
  }
184966
+ function singleAgentAdvice(context, client) {
184967
+ if (client === "claude-code" && !commandExists("codex", context.env)) {
184968
+ return [
184969
+ {
184970
+ title: "Single-agent config",
184971
+ status: "skipped",
184972
+ detail: [
184973
+ "codex was not found on PATH. To use Claude only, add:",
184974
+ "agents: {",
184975
+ " codex: { enabled: false },",
184976
+ "}",
184977
+ "Claude will run as combined_reviewer and cross-model verification will be marked unavailable."
184978
+ ].join(`
184979
+ `)
184980
+ }
184981
+ ];
184982
+ }
184983
+ if (client === "codex" && !commandExists("claude", context.env)) {
184984
+ return [
184985
+ {
184986
+ title: "Single-agent config",
184987
+ status: "skipped",
184988
+ detail: [
184989
+ "claude was not found on PATH. To use Codex only, add:",
184990
+ "agents: {",
184991
+ " claude: { enabled: false },",
184992
+ "}",
184993
+ "Codex will run as combined_reviewer and cross-model verification will be marked unavailable."
184994
+ ].join(`
184995
+ `)
184996
+ }
184997
+ ];
184998
+ }
184999
+ return [];
185000
+ }
184959
185001
  function indent(value) {
184960
185002
  return value.split(`
184961
185003
  `).map((line) => ` ${line}`).join(`
@@ -184966,6 +185008,10 @@ function shellQuote(value) {
184966
185008
  return value;
184967
185009
  return `'${value.replaceAll("'", "'\\''")}'`;
184968
185010
  }
185011
+ function commandExists(command, env) {
185012
+ const paths = env.PATH?.split(delimiter) ?? [];
185013
+ return paths.some((path) => existsSync(join2(path, command)));
185014
+ }
184969
185015
  function isMissingPathError2(error51) {
184970
185016
  return typeof error51 === "object" && error51 !== null && "code" in error51 && error51.code === "ENOENT";
184971
185017
  }
@@ -184975,8 +185021,8 @@ async function runDoctor(options) {
184975
185021
  const env = options.env ?? process.env;
184976
185022
  const loaded = await loadConfig(options);
184977
185023
  const lines = ["Kyoso doctor", "", "Runtime"];
184978
- lines.push(` Bun: ${commandExists("bun", env) ? "ok" : "warning not found"}`);
184979
- lines.push(` Node/npm: ${commandExists("npm", env) ? "ok" : "warning npm not found"}`);
185024
+ lines.push(` Bun: ${commandExists2("bun", env) ? "ok" : "warning not found"}`);
185025
+ lines.push(` Node/npm: ${commandExists2("npm", env) ? "ok" : "warning npm not found"}`);
184980
185026
  lines.push("", "Config");
184981
185027
  lines.push(` kyoso.config.ts: ${loaded.configPath ? `found ${loaded.configPath}` : "not found; using defaults"}`);
184982
185028
  lines.push(` trusted config: ${formatTrustStatus(loaded.configTrustStatus)}`);
@@ -185004,12 +185050,16 @@ async function runDoctor(options) {
185004
185050
  lines.push(" next: run `npx @kyo-so/cli setup claude-code --write`");
185005
185051
  }
185006
185052
  lines.push("", "ACP agents");
185053
+ const agentCommandExists = {
185054
+ codex: commandExists2(loaded.config.agents.codex.command, env),
185055
+ claude: commandExists2(loaded.config.agents.claude.command, env)
185056
+ };
185007
185057
  for (const agent of ["codex", "claude"]) {
185008
185058
  const config2 = loaded.config.agents[agent];
185009
- const exists3 = commandExists(config2.command, env);
185059
+ const exists3 = agentCommandExists[agent];
185010
185060
  lines.push(` ${agent === "codex" ? "Codex" : "Claude"}: ${exists3 ? "ok" : "warning command not found"}`);
185011
185061
  lines.push(` command: ${[config2.command, ...config2.args].join(" ")}`);
185012
- if (!exists3 && config2.command === "npx" && commandExists("bunx", env)) {
185062
+ if (!exists3 && config2.command === "npx" && commandExists2("bunx", env)) {
185013
185063
  lines.push(' hint: replace command "npx" with "bunx" in kyoso.config.ts');
185014
185064
  }
185015
185065
  if (agent === "claude") {
@@ -185029,6 +185079,11 @@ async function runDoctor(options) {
185029
185079
  lines.push(" auth: detected or delegated");
185030
185080
  }
185031
185081
  }
185082
+ if (agentCommandExists.codex !== agentCommandExists.claude) {
185083
+ const missing = agentCommandExists.codex ? "claude" : "codex";
185084
+ const remaining = agentCommandExists.codex ? "codex" : "claude";
185085
+ lines.push(` single-agent mode: set agents.${missing}.enabled: false to use ${remaining} only; the remaining agent will cover both review roles.`);
185086
+ }
185032
185087
  const judgeProvider = loaded.config.judge.mode === "deterministic_only" ? "deterministic_fallback" : resolveJudgeProvider(loaded.config.judge.provider, env);
185033
185088
  lines.push("", "Judge");
185034
185089
  lines.push(` provider: ${judgeProvider}`);
@@ -185063,8 +185118,8 @@ function formatClaudeDualAuthWarning(preferApiKey) {
185063
185118
  function hasEnv2(env, key) {
185064
185119
  return typeof env[key] === "string" && env[key].trim().length > 0;
185065
185120
  }
185066
- function commandExists(command, env) {
185067
- const paths = env.PATH?.split(delimiter) ?? [];
185121
+ function commandExists2(command, env) {
185122
+ const paths = env.PATH?.split(delimiter2) ?? [];
185068
185123
  return paths.some((path) => {
185069
185124
  try {
185070
185125
  accessSync(`${path}/${command}`);
@@ -198451,7 +198506,7 @@ function buildOpinion(agent, role, tool) {
198451
198506
  }
198452
198507
 
198453
198508
  // src/acp/prompts.ts
198454
- function buildAgentPrompt(tool, request, agent) {
198509
+ function buildAgentPrompt(tool, request, agent, role) {
198455
198510
  const shared = [
198456
198511
  "You are running as a Kyoso child reviewer.",
198457
198512
  "Do not edit files.",
@@ -198464,15 +198519,26 @@ function buildAgentPrompt(tool, request, agent) {
198464
198519
  "Use empty arrays when no finding, test, risk, or question exists; do not copy the example finding."
198465
198520
  ].join(`
198466
198521
  `);
198467
- const role = agent === "codex" ? [
198468
- "You are the Codex implementation reviewer in Kyoso.",
198469
- "Focus on feasibility, minimal change, existing code consistency, regression risk, tests, migration risk, and maintainability."
198470
- ].join(`
198471
- `) : [
198472
- "You are the Claude architecture and security reviewer in Kyoso.",
198473
- "Focus on architecture, threat modeling, authn/authz, secrets, privacy, secure defaults, CISA Secure by Design, and edge cases."
198474
- ].join(`
198475
- `);
198522
+ const roleInstructions = {
198523
+ implementation_reviewer: [
198524
+ "You are the implementation reviewer role in Kyoso.",
198525
+ "Focus on feasibility, minimal change, existing code consistency, regression risk, tests, migration risk, and maintainability."
198526
+ ].join(`
198527
+ `),
198528
+ architecture_security_reviewer: [
198529
+ "You are the architecture and security reviewer role in Kyoso.",
198530
+ "Focus on architecture, threat modeling, authn/authz, secrets, privacy, secure defaults, CISA Secure by Design, and edge cases."
198531
+ ].join(`
198532
+ `),
198533
+ combined_reviewer: [
198534
+ "You are the combined reviewer role in Kyoso.",
198535
+ "Cover both implementation review and architecture/security review in one pass.",
198536
+ "First assess feasibility, minimal change, existing code consistency, regression risk, tests, migration risk, and maintainability.",
198537
+ "Then assess architecture, threat modeling, authn/authz, secrets, privacy, secure defaults, CISA Secure by Design, and edge cases.",
198538
+ "Use finding category values so readers can distinguish implementation, architecture, and security concerns."
198539
+ ].join(`
198540
+ `)
198541
+ };
198476
198542
  const cisaInstruction = tool === "security_review" ? [
198477
198543
  "For security_review, include cisaMapping on each security-relevant finding when applicable.",
198478
198544
  "Also include cisaSecureByDesign with all four gate dimensions."
@@ -198480,7 +198546,9 @@ function buildAgentPrompt(tool, request, agent) {
198480
198546
  `) : "For plan_review and diff_review, include cisaMapping and cisaSecureByDesign only when relevant.";
198481
198547
  return `${shared}
198482
198548
 
198483
- ${role}
198549
+ Agent: ${agent}
198550
+ Role: ${role}
198551
+ ${roleInstructions[role]}
198484
198552
 
198485
198553
  Tool: ${tool}
198486
198554
  ${cisaInstruction}
@@ -199067,6 +199135,7 @@ function renderMarkdownResult(tool, result, options = {}) {
199067
199135
  `**Decision:** ${result.decision}`,
199068
199136
  `**Mode:** ${tool}`,
199069
199137
  `**Agents:** ${result.agentOpinions.map((opinion) => `${title(opinion.agent)} ${opinion.status}`).join(", ")}`,
199138
+ `**Review mode:** ${formatReviewMode(result)}`,
199070
199139
  `**Degraded:** ${String(result.degraded)}`,
199071
199140
  "",
199072
199141
  "## Summary",
@@ -199093,7 +199162,11 @@ function renderMarkdownResult(tool, result, options = {}) {
199093
199162
  lines.push(`### ${title(opinion.agent)}`, "", `${opinion.summary} (${opinion.status})`, "");
199094
199163
  }
199095
199164
  lines.push("", "## Disagreements", "");
199096
- lines.push(...result.disagreements.length > 0 ? result.disagreements.map((item) => `- ${item.topic}: ${item.judgeComment}`) : ["- None."]);
199165
+ if (result.reviewMode === "single_agent") {
199166
+ lines.push("- N/A - single-agent review.");
199167
+ } else {
199168
+ lines.push(...result.disagreements.length > 0 ? result.disagreements.map((item) => `- ${item.topic}: ${item.judgeComment}`) : ["- None."]);
199169
+ }
199097
199170
  lines.push("", "## Notes", "", "Kyoso did not modify files. Review was performed on a temporary snapshot.");
199098
199171
  if (result.audit.networkMode === "unrestricted") {
199099
199172
  lines.push("Network mode was unrestricted. File modification policy remained denied.");
@@ -199107,6 +199180,14 @@ function defaultSummaryText(result) {
199107
199180
  function title(value) {
199108
199181
  return value.slice(0, 1).toUpperCase() + value.slice(1);
199109
199182
  }
199183
+ function formatReviewMode(result) {
199184
+ if (result.reviewMode !== "single_agent")
199185
+ return "multi-agent";
199186
+ const opinion = result.agentOpinions[0];
199187
+ const agent = opinion ? title(opinion.agent) : "single agent";
199188
+ const role = opinion?.role === "combined_reviewer" ? "combined role" : "configured role";
199189
+ return `single-agent (${agent}, ${role}; cross-model verification was not performed)`;
199190
+ }
199110
199191
  function notes(items) {
199111
199192
  return (items[0] ?? "No notes.").replaceAll("|", "\\|");
199112
199193
  }
@@ -199333,8 +199414,8 @@ async function createSnapshot(traceId, tool, request, options = {}) {
199333
199414
  }
199334
199415
  await writeFile5(join5(contextDir, "request.json"), JSON.stringify(stripContents(request), null, 2), "utf8");
199335
199416
  await writeFile5(join5(contextDir, "selected_files_manifest.json"), JSON.stringify(buildSelectedFilesManifest(request), null, 2), "utf8");
199336
- await writeFile5(join5(contextDir, "instructions.codex.md"), buildAgentPrompt(tool, request, "codex"), "utf8");
199337
- await writeFile5(join5(contextDir, "instructions.claude.md"), buildAgentPrompt(tool, request, "claude"), "utf8");
199417
+ await writeFile5(join5(contextDir, "instructions.codex.md"), buildAgentPrompt(tool, request, "codex", options.agentRoles?.codex ?? "implementation_reviewer"), "utf8");
199418
+ await writeFile5(join5(contextDir, "instructions.claude.md"), buildAgentPrompt(tool, request, "claude", options.agentRoles?.claude ?? "architecture_security_reviewer"), "utf8");
199338
199419
  if (request.repoSummary)
199339
199420
  await writeFile5(join5(contextDir, "repo_summary.md"), request.repoSummary, "utf8");
199340
199421
  if (request.currentPlan)
@@ -199496,9 +199577,11 @@ async function runReview(tool, request, options = {}) {
199496
199577
  allowPatterns
199497
199578
  });
199498
199579
  warnings.push(...built.warnings);
199580
+ const agentRoles = resolveAgentRoles(loaded.config);
199499
199581
  snapshot = await createSnapshot(traceId, tool, built.request, {
199500
199582
  denyPatterns,
199501
- allowPatterns
199583
+ allowPatterns,
199584
+ agentRoles
199502
199585
  });
199503
199586
  await trace.write({
199504
199587
  type: "snapshot_created",
@@ -199518,6 +199601,8 @@ async function runReview(tool, request, options = {}) {
199518
199601
  trace
199519
199602
  });
199520
199603
  const normalizedAgentResults = agentResults.map(normalizeAgentRunResult);
199604
+ const agentsUsed = normalizedAgentResults.map((result2) => result2.agent);
199605
+ const reviewMode = agentsUsed.length === 1 ? "single_agent" : "multi_agent";
199521
199606
  const completed = normalizedAgentResults.filter((result2) => result2.status === "completed");
199522
199607
  const degraded = completed.length !== agentResults.length;
199523
199608
  let aggregate = aggregateAgentResults(normalizedAgentResults);
@@ -199569,6 +199654,8 @@ async function runReview(tool, request, options = {}) {
199569
199654
  const resultWithoutMarkdown = {
199570
199655
  decision,
199571
199656
  degraded,
199657
+ agentsUsed,
199658
+ reviewMode,
199572
199659
  findings: aggregate.findings,
199573
199660
  cisaSecureByDesign: cisa,
199574
199661
  disagreements: aggregate.disagreements,
@@ -199581,7 +199668,7 @@ async function runReview(tool, request, options = {}) {
199581
199668
  traceId,
199582
199669
  startedAt,
199583
199670
  completedAt,
199584
- agentsUsed: normalizedAgentResults.map((result2) => result2.agent),
199671
+ agentsUsed,
199585
199672
  redactionsApplied: secretScan.redactions,
199586
199673
  networkMode,
199587
199674
  workspaceMode: "temp_snapshot",
@@ -199640,12 +199727,13 @@ async function runReview(tool, request, options = {}) {
199640
199727
  }
199641
199728
  }
199642
199729
  async function runAgents(input2) {
199730
+ const agentRoles = resolveAgentRoles(input2.config);
199643
199731
  const agentInputs = ["codex", "claude"].filter((agent) => input2.config.agents[agent].enabled).map((agent) => ({
199644
199732
  traceId: input2.traceId,
199645
199733
  agent,
199646
- role: input2.config.agents[agent].role,
199734
+ role: agentRoles[agent] ?? input2.config.agents[agent].role,
199647
199735
  tool: input2.tool,
199648
- prompt: buildAgentPrompt(input2.tool, input2.request, agent),
199736
+ prompt: buildAgentPrompt(input2.tool, input2.request, agent, agentRoles[agent] ?? input2.config.agents[agent].role),
199649
199737
  workspaceDir: input2.workspaceDir,
199650
199738
  timeoutMs: input2.request.options?.maxAgentTimeoutMs ?? input2.config.agents[agent].timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS,
199651
199739
  networkMode: input2.networkMode
@@ -199654,6 +199742,7 @@ async function runAgents(input2) {
199654
199742
  type: "agent_started",
199655
199743
  traceId: input2.traceId,
199656
199744
  agent: agentInput.agent,
199745
+ role: agentInput.role,
199657
199746
  timestamp: new Date().toISOString()
199658
199747
  })));
199659
199748
  const results = await input2.manager.runAll(agentInputs);
@@ -199662,6 +199751,7 @@ async function runAgents(input2) {
199662
199751
  type: "agent_completed",
199663
199752
  traceId: input2.traceId,
199664
199753
  agent: result.agent,
199754
+ role: result.role,
199665
199755
  status: result.status,
199666
199756
  startedAt: result.startedAt,
199667
199757
  completedAt: result.completedAt,
@@ -199678,6 +199768,15 @@ async function runAgents(input2) {
199678
199768
  }));
199679
199769
  return results;
199680
199770
  }
199771
+ function resolveAgentRoles(config2) {
199772
+ const enabledAgents = ["codex", "claude"].filter((agent) => config2.agents[agent].enabled);
199773
+ const singleAgentMode = enabledAgents.length === 1;
199774
+ const roles = {};
199775
+ for (const agent of enabledAgents) {
199776
+ roles[agent] = singleAgentMode ? "combined_reviewer" : config2.agents[agent].role;
199777
+ }
199778
+ return roles;
199779
+ }
199681
199780
  function defaultAgentManager(config2) {
199682
199781
  if (process.env.KYOSO_TEST_FAKE_AGENTS === "1")
199683
199782
  return new FakeAgentManager;
@@ -199715,6 +199814,8 @@ async function buildSecretBlockResult(input2) {
199715
199814
  const resultWithoutMarkdown = {
199716
199815
  decision: "block",
199717
199816
  degraded: false,
199817
+ agentsUsed: [],
199818
+ reviewMode: "multi_agent",
199718
199819
  findings: [finding],
199719
199820
  cisaSecureByDesign: cisa,
199720
199821
  disagreements: [],
@@ -199795,6 +199896,8 @@ async function buildPolicyBlockResult(input2) {
199795
199896
  const resultWithoutMarkdown = {
199796
199897
  decision: "block",
199797
199898
  degraded: false,
199899
+ agentsUsed: [],
199900
+ reviewMode: "multi_agent",
199798
199901
  findings: [input2.finding],
199799
199902
  cisaSecureByDesign: input2.tool === "security_review" ? computeCisaGate([input2.finding], []) : undefined,
199800
199903
  disagreements: [],
@@ -23,6 +23,7 @@ export declare const kyosoConfigSchema: z.ZodObject<{
23
23
  role: z.ZodEnum<{
24
24
  implementation_reviewer: "implementation_reviewer";
25
25
  architecture_security_reviewer: "architecture_security_reviewer";
26
+ combined_reviewer: "combined_reviewer";
26
27
  }>;
27
28
  timeoutMs: z.ZodDefault<z.ZodNumber>;
28
29
  env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -43,6 +44,7 @@ export declare const kyosoConfigSchema: z.ZodObject<{
43
44
  role: z.ZodEnum<{
44
45
  implementation_reviewer: "implementation_reviewer";
45
46
  architecture_security_reviewer: "architecture_security_reviewer";
47
+ combined_reviewer: "combined_reviewer";
46
48
  }>;
47
49
  timeoutMs: z.ZodDefault<z.ZodNumber>;
48
50
  env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -4,4 +4,4 @@ export declare const DEFAULT_MAX_DIFF_BYTES = 300000;
4
4
  export declare const RAW_OUTPUT_MAX_CHARS = 16384;
5
5
  export declare const TRACE_DIR = ".kyoso/traces";
6
6
  export declare const KYOSO_CHILD_AGENT = "KYOSO_CHILD_AGENT";
7
- export declare const KYOSO_VERSION = "0.1.0";
7
+ export declare const KYOSO_VERSION = "0.3.0";
@@ -59,7 +59,8 @@ export type CisaSecureByDesignResult = {
59
59
  notes: string[];
60
60
  };
61
61
  export type AgentName = "codex" | "claude";
62
- export type AgentRole = "implementation_reviewer" | "architecture_security_reviewer";
62
+ export type AgentRole = "implementation_reviewer" | "architecture_security_reviewer" | "combined_reviewer";
63
+ export type ReviewMode = "multi_agent" | "single_agent";
63
64
  export type NormalizedAgentOpinion = {
64
65
  agent: AgentName;
65
66
  role: string;
@@ -110,6 +111,8 @@ export type AgentRunResult = {
110
111
  export type KyosoResult = {
111
112
  decision: KyosoDecision;
112
113
  degraded: boolean;
114
+ agentsUsed: AgentName[];
115
+ reviewMode: ReviewMode;
113
116
  summaryMarkdown: string;
114
117
  findings: KyosoFinding[];
115
118
  cisaSecureByDesign?: CisaSecureByDesignResult;
package/dist/index.js CHANGED
@@ -183783,7 +183783,11 @@ var agentSchema = exports_external.object({
183783
183783
  command: exports_external.string(),
183784
183784
  args: exports_external.array(exports_external.string()).default([]),
183785
183785
  model: exports_external.string().optional(),
183786
- role: exports_external.enum(["implementation_reviewer", "architecture_security_reviewer"]),
183786
+ role: exports_external.enum([
183787
+ "implementation_reviewer",
183788
+ "architecture_security_reviewer",
183789
+ "combined_reviewer"
183790
+ ]),
183787
183791
  timeoutMs: exports_external.number().int().positive().default(120000),
183788
183792
  env: exports_external.record(exports_external.string(), exports_external.string()).default({}),
183789
183793
  auth: exports_external.object({
@@ -188100,7 +188104,7 @@ function buildOpinion(agent, role, tool) {
188100
188104
  }
188101
188105
 
188102
188106
  // src/acp/prompts.ts
188103
- function buildAgentPrompt(tool, request, agent) {
188107
+ function buildAgentPrompt(tool, request, agent, role) {
188104
188108
  const shared = [
188105
188109
  "You are running as a Kyoso child reviewer.",
188106
188110
  "Do not edit files.",
@@ -188113,15 +188117,26 @@ function buildAgentPrompt(tool, request, agent) {
188113
188117
  "Use empty arrays when no finding, test, risk, or question exists; do not copy the example finding."
188114
188118
  ].join(`
188115
188119
  `);
188116
- const role = agent === "codex" ? [
188117
- "You are the Codex implementation reviewer in Kyoso.",
188118
- "Focus on feasibility, minimal change, existing code consistency, regression risk, tests, migration risk, and maintainability."
188119
- ].join(`
188120
- `) : [
188121
- "You are the Claude architecture and security reviewer in Kyoso.",
188122
- "Focus on architecture, threat modeling, authn/authz, secrets, privacy, secure defaults, CISA Secure by Design, and edge cases."
188123
- ].join(`
188124
- `);
188120
+ const roleInstructions = {
188121
+ implementation_reviewer: [
188122
+ "You are the implementation reviewer role in Kyoso.",
188123
+ "Focus on feasibility, minimal change, existing code consistency, regression risk, tests, migration risk, and maintainability."
188124
+ ].join(`
188125
+ `),
188126
+ architecture_security_reviewer: [
188127
+ "You are the architecture and security reviewer role in Kyoso.",
188128
+ "Focus on architecture, threat modeling, authn/authz, secrets, privacy, secure defaults, CISA Secure by Design, and edge cases."
188129
+ ].join(`
188130
+ `),
188131
+ combined_reviewer: [
188132
+ "You are the combined reviewer role in Kyoso.",
188133
+ "Cover both implementation review and architecture/security review in one pass.",
188134
+ "First assess feasibility, minimal change, existing code consistency, regression risk, tests, migration risk, and maintainability.",
188135
+ "Then assess architecture, threat modeling, authn/authz, secrets, privacy, secure defaults, CISA Secure by Design, and edge cases.",
188136
+ "Use finding category values so readers can distinguish implementation, architecture, and security concerns."
188137
+ ].join(`
188138
+ `)
188139
+ };
188125
188140
  const cisaInstruction = tool === "security_review" ? [
188126
188141
  "For security_review, include cisaMapping on each security-relevant finding when applicable.",
188127
188142
  "Also include cisaSecureByDesign with all four gate dimensions."
@@ -188129,7 +188144,9 @@ function buildAgentPrompt(tool, request, agent) {
188129
188144
  `) : "For plan_review and diff_review, include cisaMapping and cisaSecureByDesign only when relevant.";
188130
188145
  return `${shared}
188131
188146
 
188132
- ${role}
188147
+ Agent: ${agent}
188148
+ Role: ${role}
188149
+ ${roleInstructions[role]}
188133
188150
 
188134
188151
  Tool: ${tool}
188135
188152
  ${cisaInstruction}
@@ -188716,6 +188733,7 @@ function renderMarkdownResult(tool, result, options = {}) {
188716
188733
  `**Decision:** ${result.decision}`,
188717
188734
  `**Mode:** ${tool}`,
188718
188735
  `**Agents:** ${result.agentOpinions.map((opinion) => `${title(opinion.agent)} ${opinion.status}`).join(", ")}`,
188736
+ `**Review mode:** ${formatReviewMode(result)}`,
188719
188737
  `**Degraded:** ${String(result.degraded)}`,
188720
188738
  "",
188721
188739
  "## Summary",
@@ -188742,7 +188760,11 @@ function renderMarkdownResult(tool, result, options = {}) {
188742
188760
  lines.push(`### ${title(opinion.agent)}`, "", `${opinion.summary} (${opinion.status})`, "");
188743
188761
  }
188744
188762
  lines.push("", "## Disagreements", "");
188745
- lines.push(...result.disagreements.length > 0 ? result.disagreements.map((item) => `- ${item.topic}: ${item.judgeComment}`) : ["- None."]);
188763
+ if (result.reviewMode === "single_agent") {
188764
+ lines.push("- N/A - single-agent review.");
188765
+ } else {
188766
+ lines.push(...result.disagreements.length > 0 ? result.disagreements.map((item) => `- ${item.topic}: ${item.judgeComment}`) : ["- None."]);
188767
+ }
188746
188768
  lines.push("", "## Notes", "", "Kyoso did not modify files. Review was performed on a temporary snapshot.");
188747
188769
  if (result.audit.networkMode === "unrestricted") {
188748
188770
  lines.push("Network mode was unrestricted. File modification policy remained denied.");
@@ -188756,6 +188778,14 @@ function defaultSummaryText(result) {
188756
188778
  function title(value) {
188757
188779
  return value.slice(0, 1).toUpperCase() + value.slice(1);
188758
188780
  }
188781
+ function formatReviewMode(result) {
188782
+ if (result.reviewMode !== "single_agent")
188783
+ return "multi-agent";
188784
+ const opinion = result.agentOpinions[0];
188785
+ const agent = opinion ? title(opinion.agent) : "single agent";
188786
+ const role = opinion?.role === "combined_reviewer" ? "combined role" : "configured role";
188787
+ return `single-agent (${agent}, ${role}; cross-model verification was not performed)`;
188788
+ }
188759
188789
  function notes(items) {
188760
188790
  return (items[0] ?? "No notes.").replaceAll("|", "\\|");
188761
188791
  }
@@ -189209,8 +189239,8 @@ async function createSnapshot(traceId, tool, request, options = {}) {
189209
189239
  }
189210
189240
  await writeFile2(join3(contextDir, "request.json"), JSON.stringify(stripContents(request), null, 2), "utf8");
189211
189241
  await writeFile2(join3(contextDir, "selected_files_manifest.json"), JSON.stringify(buildSelectedFilesManifest(request), null, 2), "utf8");
189212
- await writeFile2(join3(contextDir, "instructions.codex.md"), buildAgentPrompt(tool, request, "codex"), "utf8");
189213
- await writeFile2(join3(contextDir, "instructions.claude.md"), buildAgentPrompt(tool, request, "claude"), "utf8");
189242
+ await writeFile2(join3(contextDir, "instructions.codex.md"), buildAgentPrompt(tool, request, "codex", options.agentRoles?.codex ?? "implementation_reviewer"), "utf8");
189243
+ await writeFile2(join3(contextDir, "instructions.claude.md"), buildAgentPrompt(tool, request, "claude", options.agentRoles?.claude ?? "architecture_security_reviewer"), "utf8");
189214
189244
  if (request.repoSummary)
189215
189245
  await writeFile2(join3(contextDir, "repo_summary.md"), request.repoSummary, "utf8");
189216
189246
  if (request.currentPlan)
@@ -189372,9 +189402,11 @@ async function runReview(tool, request, options = {}) {
189372
189402
  allowPatterns
189373
189403
  });
189374
189404
  warnings.push(...built.warnings);
189405
+ const agentRoles = resolveAgentRoles(loaded.config);
189375
189406
  snapshot = await createSnapshot(traceId, tool, built.request, {
189376
189407
  denyPatterns,
189377
- allowPatterns
189408
+ allowPatterns,
189409
+ agentRoles
189378
189410
  });
189379
189411
  await trace.write({
189380
189412
  type: "snapshot_created",
@@ -189394,6 +189426,8 @@ async function runReview(tool, request, options = {}) {
189394
189426
  trace
189395
189427
  });
189396
189428
  const normalizedAgentResults = agentResults.map(normalizeAgentRunResult);
189429
+ const agentsUsed = normalizedAgentResults.map((result2) => result2.agent);
189430
+ const reviewMode = agentsUsed.length === 1 ? "single_agent" : "multi_agent";
189397
189431
  const completed = normalizedAgentResults.filter((result2) => result2.status === "completed");
189398
189432
  const degraded = completed.length !== agentResults.length;
189399
189433
  let aggregate = aggregateAgentResults(normalizedAgentResults);
@@ -189445,6 +189479,8 @@ async function runReview(tool, request, options = {}) {
189445
189479
  const resultWithoutMarkdown = {
189446
189480
  decision,
189447
189481
  degraded,
189482
+ agentsUsed,
189483
+ reviewMode,
189448
189484
  findings: aggregate.findings,
189449
189485
  cisaSecureByDesign: cisa,
189450
189486
  disagreements: aggregate.disagreements,
@@ -189457,7 +189493,7 @@ async function runReview(tool, request, options = {}) {
189457
189493
  traceId,
189458
189494
  startedAt,
189459
189495
  completedAt,
189460
- agentsUsed: normalizedAgentResults.map((result2) => result2.agent),
189496
+ agentsUsed,
189461
189497
  redactionsApplied: secretScan.redactions,
189462
189498
  networkMode,
189463
189499
  workspaceMode: "temp_snapshot",
@@ -189516,12 +189552,13 @@ async function runReview(tool, request, options = {}) {
189516
189552
  }
189517
189553
  }
189518
189554
  async function runAgents(input) {
189555
+ const agentRoles = resolveAgentRoles(input.config);
189519
189556
  const agentInputs = ["codex", "claude"].filter((agent) => input.config.agents[agent].enabled).map((agent) => ({
189520
189557
  traceId: input.traceId,
189521
189558
  agent,
189522
- role: input.config.agents[agent].role,
189559
+ role: agentRoles[agent] ?? input.config.agents[agent].role,
189523
189560
  tool: input.tool,
189524
- prompt: buildAgentPrompt(input.tool, input.request, agent),
189561
+ prompt: buildAgentPrompt(input.tool, input.request, agent, agentRoles[agent] ?? input.config.agents[agent].role),
189525
189562
  workspaceDir: input.workspaceDir,
189526
189563
  timeoutMs: input.request.options?.maxAgentTimeoutMs ?? input.config.agents[agent].timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS,
189527
189564
  networkMode: input.networkMode
@@ -189530,6 +189567,7 @@ async function runAgents(input) {
189530
189567
  type: "agent_started",
189531
189568
  traceId: input.traceId,
189532
189569
  agent: agentInput.agent,
189570
+ role: agentInput.role,
189533
189571
  timestamp: new Date().toISOString()
189534
189572
  })));
189535
189573
  const results = await input.manager.runAll(agentInputs);
@@ -189538,6 +189576,7 @@ async function runAgents(input) {
189538
189576
  type: "agent_completed",
189539
189577
  traceId: input.traceId,
189540
189578
  agent: result.agent,
189579
+ role: result.role,
189541
189580
  status: result.status,
189542
189581
  startedAt: result.startedAt,
189543
189582
  completedAt: result.completedAt,
@@ -189554,6 +189593,15 @@ async function runAgents(input) {
189554
189593
  }));
189555
189594
  return results;
189556
189595
  }
189596
+ function resolveAgentRoles(config2) {
189597
+ const enabledAgents = ["codex", "claude"].filter((agent) => config2.agents[agent].enabled);
189598
+ const singleAgentMode = enabledAgents.length === 1;
189599
+ const roles = {};
189600
+ for (const agent of enabledAgents) {
189601
+ roles[agent] = singleAgentMode ? "combined_reviewer" : config2.agents[agent].role;
189602
+ }
189603
+ return roles;
189604
+ }
189557
189605
  function defaultAgentManager(config2) {
189558
189606
  if (process.env.KYOSO_TEST_FAKE_AGENTS === "1")
189559
189607
  return new FakeAgentManager;
@@ -189591,6 +189639,8 @@ async function buildSecretBlockResult(input) {
189591
189639
  const resultWithoutMarkdown = {
189592
189640
  decision: "block",
189593
189641
  degraded: false,
189642
+ agentsUsed: [],
189643
+ reviewMode: "multi_agent",
189594
189644
  findings: [finding],
189595
189645
  cisaSecureByDesign: cisa,
189596
189646
  disagreements: [],
@@ -189671,6 +189721,8 @@ async function buildPolicyBlockResult(input) {
189671
189721
  const resultWithoutMarkdown = {
189672
189722
  decision: "block",
189673
189723
  degraded: false,
189724
+ agentsUsed: [],
189725
+ reviewMode: "multi_agent",
189674
189726
  findings: [input.finding],
189675
189727
  cisaSecureByDesign: input.tool === "security_review" ? computeCisaGate([input.finding], []) : undefined,
189676
189728
  disagreements: [],
@@ -1,4 +1,4 @@
1
- import type { KyosoReviewRequest, ReviewTool } from "../core/types.js";
1
+ import type { AgentName, AgentRole, KyosoReviewRequest, ReviewTool } from "../core/types.js";
2
2
  export type Snapshot = {
3
3
  root: string;
4
4
  repoDir: string;
@@ -8,4 +8,5 @@ export type Snapshot = {
8
8
  export declare function createSnapshot(traceId: string, tool: ReviewTool, request: KyosoReviewRequest, options?: {
9
9
  denyPatterns?: string[];
10
10
  allowPatterns?: string[];
11
+ agentRoles?: Partial<Record<AgentName, AgentRole>>;
11
12
  }): Promise<Snapshot>;
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "@kyo-so/cli";
2
+
3
+ export default defineConfig({
4
+ agents: {
5
+ codex: {
6
+ enabled: false,
7
+ },
8
+ },
9
+ });
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "@kyo-so/cli";
2
+
3
+ export default defineConfig({
4
+ agents: {
5
+ claude: {
6
+ enabled: false,
7
+ },
8
+ },
9
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyo-so/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Kyo-so: MCP-native, ACP-powered multi-agent review gates for AI coding workflows.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",