@kyo-so/cli 0.1.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/dist/bin/kyoso.js CHANGED
@@ -169495,18 +169495,21 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
169495
169495
  });
169496
169496
 
169497
169497
  // src/cli/main.ts
169498
- import { spawnSync } from "node:child_process";
169498
+ import { spawnSync as spawnSync2 } from "node:child_process";
169499
169499
 
169500
169500
  // src/cli/args.ts
169501
169501
  function parseArgs(argv) {
169502
169502
  const [command = "help", ...rest] = argv;
169503
+ const positionals = [];
169503
169504
  const flags = {};
169504
169505
  for (let index = 0;index < rest.length; index += 1) {
169505
169506
  const item = rest[index];
169506
169507
  if (!item)
169507
169508
  continue;
169508
- if (!item.startsWith("--"))
169509
+ if (!item.startsWith("--")) {
169510
+ positionals.push(item);
169509
169511
  continue;
169512
+ }
169510
169513
  const key = item.slice(2);
169511
169514
  const next = rest[index + 1];
169512
169515
  const value = next && !next.startsWith("--") ? next : true;
@@ -169521,7 +169524,7 @@ function parseArgs(argv) {
169521
169524
  flags[key] = [String(existing), String(value)];
169522
169525
  }
169523
169526
  }
169524
- return { command, flags };
169527
+ return { command, positionals, flags };
169525
169528
  }
169526
169529
  function stringFlag(flags, key) {
169527
169530
  const value = flags[key];
@@ -169597,7 +169600,7 @@ function languageFromPath(path) {
169597
169600
 
169598
169601
  // src/cli/doctor.ts
169599
169602
  import { accessSync } from "node:fs";
169600
- import { delimiter } from "node:path";
169603
+ import { delimiter as delimiter2 } from "node:path";
169601
169604
 
169602
169605
  // src/config/loadConfig.ts
169603
169606
  import { access as access2, readFile as readFile3 } from "node:fs/promises";
@@ -184010,7 +184013,11 @@ var agentSchema = exports_external.object({
184010
184013
  command: exports_external.string(),
184011
184014
  args: exports_external.array(exports_external.string()).default([]),
184012
184015
  model: exports_external.string().optional(),
184013
- 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
+ ]),
184014
184021
  timeoutMs: exports_external.number().int().positive().default(120000),
184015
184022
  env: exports_external.record(exports_external.string(), exports_external.string()).default({}),
184016
184023
  auth: exports_external.object({
@@ -184290,7 +184297,7 @@ var REDACTION = "[KYOSO_REDACTED]";
184290
184297
  var DEFAULT_AGENT_TIMEOUT_MS = 120000;
184291
184298
  var RAW_OUTPUT_MAX_CHARS = 16384;
184292
184299
  var KYOSO_CHILD_AGENT = "KYOSO_CHILD_AGENT";
184293
- var KYOSO_VERSION = "0.1.0";
184300
+ var KYOSO_VERSION = "0.3.0";
184294
184301
 
184295
184302
  // src/security/sanitizeText.ts
184296
184303
  var SENSITIVE_TEXT_PATTERNS = [
@@ -184547,13 +184554,475 @@ function hasEnv(env, key) {
184547
184554
  return typeof env[key] === "string" && env[key].trim().length > 0;
184548
184555
  }
184549
184556
 
184557
+ // src/cli/setup.ts
184558
+ import { spawnSync } from "node:child_process";
184559
+ import { existsSync, readFileSync } from "node:fs";
184560
+ import { cp, mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
184561
+ import { homedir as homedir2 } from "node:os";
184562
+ import { delimiter, dirname as dirname4, join as join2 } from "node:path";
184563
+ import { fileURLToPath } from "node:url";
184564
+ async function runSetup(options) {
184565
+ const client = parseClient(options.client);
184566
+ const runner = parseRunner(options.runner);
184567
+ const command = options.command ? parseCommandSpec(options.command) : commandForRunner(runner);
184568
+ const context = {
184569
+ cwd: options.cwd,
184570
+ home: options.env?.HOME ?? homedir2(),
184571
+ env: options.env ?? process.env,
184572
+ write: options.write,
184573
+ scope: options.global ? "global" : "project",
184574
+ mcpCommand: command,
184575
+ sourceSkillDir: resolveBundledSkillDir()
184576
+ };
184577
+ if (!client)
184578
+ return renderSetupOverview(context);
184579
+ if (client === "codex")
184580
+ return renderResults(await setupCodex(context));
184581
+ return renderResults(await setupClaudeCode(context));
184582
+ }
184583
+ function commandForRunner(runner) {
184584
+ if (runner === "bunx") {
184585
+ return { command: "bunx", args: ["@kyo-so/cli", "mcp"] };
184586
+ }
184587
+ return { command: "npx", args: ["-y", "@kyo-so/cli", "mcp"] };
184588
+ }
184589
+ function buildCodexMcpToml(command) {
184590
+ return [
184591
+ "[mcp_servers.kyoso]",
184592
+ `command = ${JSON.stringify(command.command)}`,
184593
+ `args = ${JSON.stringify(command.args)}`,
184594
+ 'env_vars = ["OPENAI_API_KEY", "CODEX_API_KEY", "ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"]',
184595
+ "startup_timeout_sec = 20",
184596
+ "tool_timeout_sec = 360",
184597
+ "enabled = true",
184598
+ ""
184599
+ ].join(`
184600
+ `);
184601
+ }
184602
+ function buildClaudeMcpEntry(command) {
184603
+ return {
184604
+ command: command.command,
184605
+ args: command.args,
184606
+ env: {
184607
+ OPENAI_API_KEY: "${OPENAI_API_KEY}",
184608
+ ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY}",
184609
+ CLAUDE_CODE_OAUTH_TOKEN: "${CLAUDE_CODE_OAUTH_TOKEN}"
184610
+ }
184611
+ };
184612
+ }
184613
+ function skillDestination(client, scope, cwd, home) {
184614
+ if (client === "codex") {
184615
+ const root2 = scope === "global" ? home : cwd;
184616
+ return join2(root2, ".agents", "skills", "kyoso-review");
184617
+ }
184618
+ const root = scope === "global" ? home : cwd;
184619
+ return join2(root, ".claude", "skills", "kyoso-review");
184620
+ }
184621
+ function detectSetup(options) {
184622
+ const home = options.home ?? homedir2();
184623
+ return {
184624
+ codex: {
184625
+ mcp: hasCodexMcp(join2(home, ".codex", "config.toml")),
184626
+ skill: existsSync(join2(options.cwd, ".agents", "skills", "kyoso-review", "SKILL.md")) || existsSync(join2(home, ".agents", "skills", "kyoso-review", "SKILL.md"))
184627
+ },
184628
+ "claude-code": {
184629
+ mcp: hasClaudeMcp(join2(options.cwd, ".mcp.json")) || hasClaudeMcp(join2(home, ".claude.json")),
184630
+ skill: existsSync(join2(options.cwd, ".claude", "skills", "kyoso-review", "SKILL.md")) || existsSync(join2(home, ".claude", "skills", "kyoso-review", "SKILL.md"))
184631
+ }
184632
+ };
184633
+ }
184634
+ async function setupCodex(context) {
184635
+ return [
184636
+ await ensureCodexMcp(context),
184637
+ await ensureSkill({
184638
+ title: "Codex skill",
184639
+ sourceDir: context.sourceSkillDir,
184640
+ destinationDir: skillDestination("codex", context.scope, context.cwd, context.home),
184641
+ write: context.write
184642
+ }),
184643
+ ...singleAgentAdvice(context, "codex")
184644
+ ];
184645
+ }
184646
+ async function setupClaudeCode(context) {
184647
+ return [
184648
+ await ensureClaudeMcp(context),
184649
+ await ensureSkill({
184650
+ title: "Claude Code skill",
184651
+ sourceDir: context.sourceSkillDir,
184652
+ destinationDir: skillDestination("claude-code", context.scope, context.cwd, context.home),
184653
+ write: context.write
184654
+ }),
184655
+ ...singleAgentAdvice(context, "claude-code")
184656
+ ];
184657
+ }
184658
+ async function ensureCodexMcp(context) {
184659
+ const configPath = join2(context.home, ".codex", "config.toml");
184660
+ const snippet = buildCodexMcpToml(context.mcpCommand);
184661
+ const current = await readOptionalFile(configPath);
184662
+ if (hasCodexMcpContent(current)) {
184663
+ return {
184664
+ title: "Codex MCP",
184665
+ status: "skipped",
184666
+ path: configPath,
184667
+ detail: "existing [mcp_servers.kyoso] kept"
184668
+ };
184669
+ }
184670
+ const detail = diffForAppend(configPath, snippet);
184671
+ if (!context.write) {
184672
+ return { title: "Codex MCP", status: "dry-run", path: configPath, detail };
184673
+ }
184674
+ const separator = current.length > 0 && !current.endsWith(`
184675
+ `) ? `
184676
+
184677
+ ` : "";
184678
+ await mkdir3(dirname4(configPath), { recursive: true });
184679
+ await writeFile3(configPath, `${current}${separator}${snippet}`, "utf8");
184680
+ return {
184681
+ title: "Codex MCP",
184682
+ status: current.length > 0 ? "updated" : "created",
184683
+ path: configPath,
184684
+ detail
184685
+ };
184686
+ }
184687
+ async function ensureClaudeMcp(context) {
184688
+ if (context.scope === "global") {
184689
+ return ensureClaudeGlobalMcp(context);
184690
+ }
184691
+ const configPath = join2(context.cwd, ".mcp.json");
184692
+ const current = await readJsonObject(configPath);
184693
+ const mcpServers = recordValue(current.mcpServers);
184694
+ if (isRecord4(mcpServers.kyoso)) {
184695
+ return {
184696
+ title: "Claude Code MCP",
184697
+ status: "skipped",
184698
+ path: configPath,
184699
+ detail: "existing mcpServers.kyoso kept"
184700
+ };
184701
+ }
184702
+ const next = {
184703
+ ...current,
184704
+ mcpServers: {
184705
+ ...mcpServers,
184706
+ kyoso: buildClaudeMcpEntry(context.mcpCommand)
184707
+ }
184708
+ };
184709
+ const detail = diffForJson(configPath, current, next);
184710
+ if (!context.write) {
184711
+ return {
184712
+ title: "Claude Code MCP",
184713
+ status: "dry-run",
184714
+ path: configPath,
184715
+ detail
184716
+ };
184717
+ }
184718
+ await writeFile3(configPath, `${JSON.stringify(next, null, 2)}
184719
+ `, "utf8");
184720
+ return {
184721
+ title: "Claude Code MCP",
184722
+ status: Object.keys(current).length > 0 ? "updated" : "created",
184723
+ path: configPath,
184724
+ detail
184725
+ };
184726
+ }
184727
+ function ensureClaudeGlobalMcp(context) {
184728
+ const configPath = join2(context.home, ".claude.json");
184729
+ if (hasClaudeMcp(configPath)) {
184730
+ return {
184731
+ title: "Claude Code MCP",
184732
+ status: "skipped",
184733
+ path: configPath,
184734
+ detail: "existing mcpServers.kyoso kept"
184735
+ };
184736
+ }
184737
+ const json2 = JSON.stringify(buildClaudeMcpEntry(context.mcpCommand));
184738
+ const args = ["mcp", "add-json", "kyoso", json2, "--scope", "user"];
184739
+ const commandLine = ["claude", ...args.map(shellQuote)].join(" ");
184740
+ if (!context.write) {
184741
+ return {
184742
+ title: "Claude Code MCP",
184743
+ status: "dry-run",
184744
+ path: configPath,
184745
+ detail: commandLine
184746
+ };
184747
+ }
184748
+ const result = spawnSync("claude", args, { encoding: "utf8" });
184749
+ if (result.status !== 0) {
184750
+ throw new Error(result.stderr || result.stdout || "claude mcp add-json failed");
184751
+ }
184752
+ return {
184753
+ title: "Claude Code MCP",
184754
+ status: "updated",
184755
+ path: configPath,
184756
+ detail: commandLine
184757
+ };
184758
+ }
184759
+ async function ensureSkill(options) {
184760
+ const destinationSkill = join2(options.destinationDir, "SKILL.md");
184761
+ if (existsSync(destinationSkill)) {
184762
+ return {
184763
+ title: options.title,
184764
+ status: "skipped",
184765
+ path: options.destinationDir,
184766
+ detail: "existing kyoso-review skill kept"
184767
+ };
184768
+ }
184769
+ const detail = [
184770
+ `copy ${options.sourceDir}`,
184771
+ `to ${options.destinationDir}`
184772
+ ].join(`
184773
+ `);
184774
+ if (!options.write) {
184775
+ return {
184776
+ title: options.title,
184777
+ status: "dry-run",
184778
+ path: options.destinationDir,
184779
+ detail
184780
+ };
184781
+ }
184782
+ await mkdir3(dirname4(options.destinationDir), { recursive: true });
184783
+ await cp(options.sourceDir, options.destinationDir, {
184784
+ recursive: true,
184785
+ force: false
184786
+ });
184787
+ return {
184788
+ title: options.title,
184789
+ status: "created",
184790
+ path: options.destinationDir,
184791
+ detail
184792
+ };
184793
+ }
184794
+ function renderSetupOverview(context) {
184795
+ const detected = detectSetup({ cwd: context.cwd, home: context.home });
184796
+ return [
184797
+ "Kyoso setup",
184798
+ "",
184799
+ "Clients",
184800
+ ` codex: MCP ${statusWord(detected.codex.mcp)}, skill ${statusWord(detected.codex.skill)}`,
184801
+ ` claude-code: MCP ${statusWord(detected["claude-code"].mcp)}, skill ${statusWord(detected["claude-code"].skill)}`,
184802
+ "",
184803
+ "Commands",
184804
+ " kyoso setup codex [--write] [--runner npx|bunx] [--global]",
184805
+ " kyoso setup claude-code [--write] [--runner npx|bunx] [--global]",
184806
+ "",
184807
+ `Default MCP command: ${context.mcpCommand.command} ${context.mcpCommand.args.join(" ")}`,
184808
+ "Dry-run is the default. Add --write to modify files."
184809
+ ].join(`
184810
+ `);
184811
+ }
184812
+ function renderResults(results) {
184813
+ return [
184814
+ "Kyoso setup",
184815
+ "",
184816
+ ...results.flatMap((result) => [
184817
+ `${result.title}: ${result.status}${result.path ? ` (${result.path})` : ""}`,
184818
+ ...result.detail ? indent(result.detail).split(`
184819
+ `) : []
184820
+ ])
184821
+ ].join(`
184822
+ `);
184823
+ }
184824
+ function parseClient(client) {
184825
+ if (client === undefined)
184826
+ return;
184827
+ if (client === "codex" || client === "claude-code")
184828
+ return client;
184829
+ throw new Error(`Invalid setup client "${client}". Expected codex or claude-code.`);
184830
+ }
184831
+ function parseRunner(runner) {
184832
+ if (runner === undefined || runner === "npx")
184833
+ return "npx";
184834
+ if (runner === "bunx")
184835
+ return "bunx";
184836
+ throw new Error(`Invalid --runner value "${runner}". Expected npx or bunx.`);
184837
+ }
184838
+ function parseCommandSpec(value) {
184839
+ const parts = splitCommand(value);
184840
+ const [command, ...args] = parts;
184841
+ if (!command)
184842
+ throw new Error("--command must not be empty");
184843
+ return { command, args };
184844
+ }
184845
+ function splitCommand(value) {
184846
+ const parts = [];
184847
+ let current = "";
184848
+ let quote;
184849
+ for (const char of value.trim()) {
184850
+ if (quote) {
184851
+ if (char === quote) {
184852
+ quote = undefined;
184853
+ } else {
184854
+ current += char;
184855
+ }
184856
+ continue;
184857
+ }
184858
+ if (char === '"' || char === "'") {
184859
+ quote = char;
184860
+ continue;
184861
+ }
184862
+ if (/\s/.test(char)) {
184863
+ if (current.length > 0) {
184864
+ parts.push(current);
184865
+ current = "";
184866
+ }
184867
+ continue;
184868
+ }
184869
+ current += char;
184870
+ }
184871
+ if (quote)
184872
+ throw new Error("--command has an unterminated quote");
184873
+ if (current.length > 0)
184874
+ parts.push(current);
184875
+ return parts;
184876
+ }
184877
+ function resolveBundledSkillDir() {
184878
+ const start = dirname4(fileURLToPath(import.meta.url));
184879
+ let current = start;
184880
+ for (let depth = 0;depth < 5; depth += 1) {
184881
+ const candidate = join2(current, ".agents", "skills", "kyoso-review");
184882
+ if (existsSync(join2(candidate, "SKILL.md")))
184883
+ return candidate;
184884
+ current = dirname4(current);
184885
+ }
184886
+ throw new Error("Bundled kyoso-review skill was not found in this package.");
184887
+ }
184888
+ async function readOptionalFile(path) {
184889
+ try {
184890
+ return await readFile4(path, "utf8");
184891
+ } catch (error51) {
184892
+ if (isMissingPathError2(error51))
184893
+ return "";
184894
+ throw error51;
184895
+ }
184896
+ }
184897
+ async function readJsonObject(path) {
184898
+ const content = await readOptionalFile(path);
184899
+ if (content.trim().length === 0)
184900
+ return {};
184901
+ const parsed = JSON.parse(content);
184902
+ if (!isRecord4(parsed))
184903
+ throw new Error(`${path} must contain a JSON object`);
184904
+ return parsed;
184905
+ }
184906
+ function hasCodexMcp(path) {
184907
+ return existsSync(path) && hasCodexMcpContent(readTextSync(path));
184908
+ }
184909
+ function hasCodexMcpContent(content) {
184910
+ return /^\s*\[mcp_servers\.(?:"kyoso"|kyoso)]\s*$/m.test(content);
184911
+ }
184912
+ function hasClaudeMcp(path) {
184913
+ if (!existsSync(path))
184914
+ return false;
184915
+ try {
184916
+ const parsed = JSON.parse(readTextSync(path));
184917
+ return jsonHasKyosoMcp(parsed);
184918
+ } catch {
184919
+ return false;
184920
+ }
184921
+ }
184922
+ function jsonHasKyosoMcp(value) {
184923
+ if (!isRecord4(value))
184924
+ return false;
184925
+ if (isRecord4(value.mcpServers) && isRecord4(value.mcpServers.kyoso)) {
184926
+ return true;
184927
+ }
184928
+ return Object.values(value).some((child) => jsonHasKyosoMcp(child));
184929
+ }
184930
+ function readTextSync(path) {
184931
+ return readFileSync(path, "utf8");
184932
+ }
184933
+ function recordValue(value) {
184934
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
184935
+ }
184936
+ function isRecord4(value) {
184937
+ return typeof value === "object" && value !== null && !Array.isArray(value);
184938
+ }
184939
+ function diffForAppend(path, snippet) {
184940
+ return [
184941
+ `--- ${path}`,
184942
+ `+++ ${path}`,
184943
+ "@@",
184944
+ ...snippet.split(`
184945
+ `).map((line) => `+${line}`)
184946
+ ].join(`
184947
+ `);
184948
+ }
184949
+ function diffForJson(path, before, after) {
184950
+ const beforeText = JSON.stringify(before, null, 2).split(`
184951
+ `);
184952
+ const afterText = JSON.stringify(after, null, 2).split(`
184953
+ `);
184954
+ return [
184955
+ `--- ${path}`,
184956
+ `+++ ${path}`,
184957
+ "@@",
184958
+ ...beforeText.map((line) => `-${line}`),
184959
+ ...afterText.map((line) => `+${line}`)
184960
+ ].join(`
184961
+ `);
184962
+ }
184963
+ function statusWord(value) {
184964
+ return value ? "ok" : "missing";
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
+ }
185001
+ function indent(value) {
185002
+ return value.split(`
185003
+ `).map((line) => ` ${line}`).join(`
185004
+ `);
185005
+ }
185006
+ function shellQuote(value) {
185007
+ if (/^[A-Za-z0-9_./:=@{}$,-]+$/.test(value))
185008
+ return value;
185009
+ return `'${value.replaceAll("'", "'\\''")}'`;
185010
+ }
185011
+ function commandExists(command, env) {
185012
+ const paths = env.PATH?.split(delimiter) ?? [];
185013
+ return paths.some((path) => existsSync(join2(path, command)));
185014
+ }
185015
+ function isMissingPathError2(error51) {
185016
+ return typeof error51 === "object" && error51 !== null && "code" in error51 && error51.code === "ENOENT";
185017
+ }
185018
+
184550
185019
  // src/cli/doctor.ts
184551
185020
  async function runDoctor(options) {
184552
185021
  const env = options.env ?? process.env;
184553
185022
  const loaded = await loadConfig(options);
184554
185023
  const lines = ["Kyoso doctor", "", "Runtime"];
184555
- lines.push(` Bun: ${commandExists("bun", env) ? "ok" : "warning not found"}`);
184556
- 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"}`);
184557
185026
  lines.push("", "Config");
184558
185027
  lines.push(` kyoso.config.ts: ${loaded.configPath ? `found ${loaded.configPath}` : "not found; using defaults"}`);
184559
185028
  lines.push(` trusted config: ${formatTrustStatus(loaded.configTrustStatus)}`);
@@ -184561,14 +185030,36 @@ async function runDoctor(options) {
184561
185030
  lines.push(` config hash: ${loaded.configHash}`);
184562
185031
  for (const warning of loaded.warnings)
184563
185032
  lines.push(` warning: ${warning}`);
185033
+ const setup = detectSetup({ cwd: options.cwd, home: env.HOME });
184564
185034
  lines.push("", "MCP", " stdio server: ok");
185035
+ lines.push(` Codex registration: ${setup.codex.mcp ? "ok" : "missing"}`);
185036
+ lines.push(` Claude Code registration: ${setup["claude-code"].mcp ? "ok" : "missing"}`);
185037
+ if (!setup.codex.mcp) {
185038
+ lines.push(" next: run `npx @kyo-so/cli setup codex --write`");
185039
+ }
185040
+ if (!setup["claude-code"].mcp) {
185041
+ lines.push(" next: run `npx @kyo-so/cli setup claude-code --write`");
185042
+ }
185043
+ lines.push("", "Skills");
185044
+ lines.push(` Codex kyoso-review: ${setup.codex.skill ? "ok" : "missing"}`);
185045
+ lines.push(` Claude Code kyoso-review: ${setup["claude-code"].skill ? "ok" : "missing"}`);
185046
+ if (!setup.codex.skill) {
185047
+ lines.push(" next: run `npx @kyo-so/cli setup codex --write`");
185048
+ }
185049
+ if (!setup["claude-code"].skill) {
185050
+ lines.push(" next: run `npx @kyo-so/cli setup claude-code --write`");
185051
+ }
184565
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
+ };
184566
185057
  for (const agent of ["codex", "claude"]) {
184567
185058
  const config2 = loaded.config.agents[agent];
184568
- const exists3 = commandExists(config2.command, env);
185059
+ const exists3 = agentCommandExists[agent];
184569
185060
  lines.push(` ${agent === "codex" ? "Codex" : "Claude"}: ${exists3 ? "ok" : "warning command not found"}`);
184570
185061
  lines.push(` command: ${[config2.command, ...config2.args].join(" ")}`);
184571
- if (!exists3 && config2.command === "npx" && commandExists("bunx", env)) {
185062
+ if (!exists3 && config2.command === "npx" && commandExists2("bunx", env)) {
184572
185063
  lines.push(' hint: replace command "npx" with "bunx" in kyoso.config.ts');
184573
185064
  }
184574
185065
  if (agent === "claude") {
@@ -184588,6 +185079,11 @@ async function runDoctor(options) {
184588
185079
  lines.push(" auth: detected or delegated");
184589
185080
  }
184590
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
+ }
184591
185087
  const judgeProvider = loaded.config.judge.mode === "deterministic_only" ? "deterministic_fallback" : resolveJudgeProvider(loaded.config.judge.provider, env);
184592
185088
  lines.push("", "Judge");
184593
185089
  lines.push(` provider: ${judgeProvider}`);
@@ -184622,8 +185118,8 @@ function formatClaudeDualAuthWarning(preferApiKey) {
184622
185118
  function hasEnv2(env, key) {
184623
185119
  return typeof env[key] === "string" && env[key].trim().length > 0;
184624
185120
  }
184625
- function commandExists(command, env) {
184626
- const paths = env.PATH?.split(delimiter) ?? [];
185121
+ function commandExists2(command, env) {
185122
+ const paths = env.PATH?.split(delimiter2) ?? [];
184627
185123
  return paths.some((path) => {
184628
185124
  try {
184629
185125
  accessSync(`${path}/${command}`);
@@ -184635,12 +185131,12 @@ function commandExists(command, env) {
184635
185131
  }
184636
185132
 
184637
185133
  // src/cli/init.ts
184638
- import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
184639
- import { join as join2 } from "node:path";
185134
+ import { readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
185135
+ import { join as join3 } from "node:path";
184640
185136
  async function runInit(options) {
184641
- const configPath = join2(options.cwd, "kyoso.config.ts");
184642
- const skillPath = join2(options.cwd, ".agents/skills/kyoso-review/SKILL.md");
184643
- const gitignorePath = join2(options.cwd, ".gitignore");
185137
+ const configPath = join3(options.cwd, "kyoso.config.ts");
185138
+ const skillPath = join3(options.cwd, ".agents/skills/kyoso-review/SKILL.md");
185139
+ const gitignorePath = join3(options.cwd, ".gitignore");
184644
185140
  const configResult = await writeFileWithOverwritePrompt(configPath, CONFIG_TEMPLATE, options.force);
184645
185141
  const skillResult = await writeFileWithOverwritePrompt(skillPath, SKILL_TEMPLATE, options.force);
184646
185142
  const gitignoreResult = await ensureGitignoreEntry(gitignorePath, ".kyoso/");
@@ -184654,11 +185150,11 @@ async function runInit(options) {
184654
185150
  async function ensureGitignoreEntry(path, entry) {
184655
185151
  let content = "";
184656
185152
  try {
184657
- content = await readFile4(path, "utf8");
185153
+ content = await readFile5(path, "utf8");
184658
185154
  } catch (error51) {
184659
- if (!isMissingPathError2(error51))
185155
+ if (!isMissingPathError3(error51))
184660
185156
  throw error51;
184661
- await writeFile3(path, `${entry}
185157
+ await writeFile4(path, `${entry}
184662
185158
  `, "utf8");
184663
185159
  return "created";
184664
185160
  }
@@ -184668,11 +185164,11 @@ async function ensureGitignoreEntry(path, entry) {
184668
185164
  const separator = content.length > 0 && !content.endsWith(`
184669
185165
  `) ? `
184670
185166
  ` : "";
184671
- await writeFile3(path, `${content}${separator}${entry}
185167
+ await writeFile4(path, `${content}${separator}${entry}
184672
185168
  `, "utf8");
184673
185169
  return "updated";
184674
185170
  }
184675
- function isMissingPathError2(error51) {
185171
+ function isMissingPathError3(error51) {
184676
185172
  return typeof error51 === "object" && error51 !== null && "code" in error51 && error51.code === "ENOENT";
184677
185173
  }
184678
185174
  var CONFIG_TEMPLATE = `import { defineConfig } from "@kyo-so/cli";
@@ -186479,7 +186975,7 @@ var Protocol = class {
186479
186975
  const { relatedRequestId, resumptionToken, onresumptiontoken } = options ?? {};
186480
186976
  let onAbort;
186481
186977
  let cleanupMessageId;
186482
- return new Promise((resolve3, reject) => {
186978
+ return new Promise((resolve4, reject) => {
186483
186979
  const earlyReject = (error51) => {
186484
186980
  reject(error51);
186485
186981
  };
@@ -186539,7 +187035,7 @@ var Protocol = class {
186539
187035
  return reject(response);
186540
187036
  validateStandardSchema(resultSchema, response.result).then((parseResult) => {
186541
187037
  if (parseResult.success)
186542
- resolve3(parseResult.data);
187038
+ resolve4(parseResult.data);
186543
187039
  else
186544
187040
  reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
186545
187041
  }, reject);
@@ -189617,7 +190113,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin((exports) => {
189617
190113
  const schOrFunc = root.refs[ref];
189618
190114
  if (schOrFunc)
189619
190115
  return schOrFunc;
189620
- let _sch = resolve3.call(this, root, ref);
190116
+ let _sch = resolve4.call(this, root, ref);
189621
190117
  if (_sch === undefined) {
189622
190118
  const schema = (_a3 = root.localRefs) === null || _a3 === undefined ? undefined : _a3[ref];
189623
190119
  const { schemaId } = this.opts;
@@ -189648,7 +190144,7 @@ var require_compile = /* @__PURE__ */ __commonJSMin((exports) => {
189648
190144
  function sameSchemaEnv(s1, s2) {
189649
190145
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
189650
190146
  }
189651
- function resolve3(root, ref) {
190147
+ function resolve4(root, ref) {
189652
190148
  let sch;
189653
190149
  while (typeof (sch = this.refs[ref]) == "string")
189654
190150
  ref = sch;
@@ -190154,7 +190650,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin((exports, module) => {
190154
190650
  uri = parse5(serialize(uri, options), options);
190155
190651
  return uri;
190156
190652
  }
190157
- function resolve3(baseURI, relativeURI, options) {
190653
+ function resolve4(baseURI, relativeURI, options) {
190158
190654
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
190159
190655
  const resolved = resolveComponent(parse5(baseURI, schemelessOptions), parse5(relativeURI, schemelessOptions), schemelessOptions, true);
190160
190656
  schemelessOptions.skipEscape = true;
@@ -190365,7 +190861,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin((exports, module) => {
190365
190861
  const fastUri = {
190366
190862
  SCHEMES,
190367
190863
  normalize,
190368
- resolve: resolve3,
190864
+ resolve: resolve4,
190369
190865
  resolveComponent,
190370
190866
  equal,
190371
190867
  serialize,
@@ -194087,7 +194583,7 @@ var StdioServerTransport = class {
194087
194583
  send(message) {
194088
194584
  if (this._closed)
194089
194585
  return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
194090
- return new Promise((resolve3, reject) => {
194586
+ return new Promise((resolve4, reject) => {
194091
194587
  const json2 = serializeMessage(message);
194092
194588
  let settled = false;
194093
194589
  const onError = (error51) => {
@@ -194104,7 +194600,7 @@ var StdioServerTransport = class {
194104
194600
  settled = true;
194105
194601
  this._stdout.off("error", onError);
194106
194602
  this._stdout.off("drain", onDrain);
194107
- resolve3();
194603
+ resolve4();
194108
194604
  };
194109
194605
  this._stdout.once("error", onError);
194110
194606
  if (this._stdout.write(json2)) {
@@ -194112,7 +194608,7 @@ var StdioServerTransport = class {
194112
194608
  return;
194113
194609
  settled = true;
194114
194610
  this._stdout.off("error", onError);
194115
- resolve3();
194611
+ resolve4();
194116
194612
  } else if (!settled)
194117
194613
  this._stdout.once("drain", onDrain);
194118
194614
  });
@@ -194120,12 +194616,12 @@ var StdioServerTransport = class {
194120
194616
  };
194121
194617
 
194122
194618
  // src/core/runReview.ts
194123
- import { resolve as resolve4 } from "node:path";
194619
+ import { resolve as resolve5 } from "node:path";
194124
194620
 
194125
194621
  // src/acp/AcpAgentProcess.ts
194126
194622
  import { spawn } from "node:child_process";
194127
- import { readFile as readFile5, realpath } from "node:fs/promises";
194128
- import { isAbsolute, relative, resolve as resolve3 } from "node:path";
194623
+ import { readFile as readFile6, realpath } from "node:fs/promises";
194624
+ import { isAbsolute, relative, resolve as resolve4 } from "node:path";
194129
194625
  import { Readable, Writable } from "node:stream";
194130
194626
 
194131
194627
  // node_modules/@agentclientprotocol/sdk/dist/schema/index.js
@@ -195999,14 +196495,14 @@ function ndJsonStream(output2, input2) {
195999
196495
  }
196000
196496
  // node_modules/@agentclientprotocol/sdk/dist/jsonrpc.js
196001
196497
  var CANCEL_REQUEST_METHOD = "$/cancel_request";
196002
- function isRecord4(value) {
196498
+ function isRecord5(value) {
196003
196499
  return typeof value === "object" && value !== null;
196004
196500
  }
196005
196501
  function isJsonRpcId(value) {
196006
196502
  return value === null || typeof value === "string" || typeof value === "number" && Number.isFinite(value);
196007
196503
  }
196008
196504
  function cancelRequestId(params) {
196009
- if (!isRecord4(params) || !isJsonRpcId(params["requestId"])) {
196505
+ if (!isRecord5(params) || !isJsonRpcId(params["requestId"])) {
196010
196506
  return;
196011
196507
  }
196012
196508
  return params["requestId"];
@@ -196230,12 +196726,12 @@ class Connection {
196230
196726
  }
196231
196727
  const id = this.nextRequestId++;
196232
196728
  let cancel = () => {};
196233
- const responsePromise = new Promise((resolve3, reject) => {
196729
+ const responsePromise = new Promise((resolve4, reject) => {
196234
196730
  const pendingResponse = {
196235
196731
  resolve: (response) => {
196236
196732
  try {
196237
196733
  const value = mapResponse ? mapResponse(response) : response;
196238
- resolve3(value);
196734
+ resolve4(value);
196239
196735
  } catch (error51) {
196240
196736
  reject(error51);
196241
196737
  }
@@ -196300,8 +196796,8 @@ class Connection {
196300
196796
  initialize(stream, handlers) {
196301
196797
  this.stream = stream;
196302
196798
  this.staticHandlers = handlers;
196303
- this.closedPromise = new Promise((resolve3) => {
196304
- this.abortController.signal.addEventListener("abort", () => resolve3());
196799
+ this.closedPromise = new Promise((resolve4) => {
196800
+ this.abortController.signal.addEventListener("abort", () => resolve4());
196305
196801
  });
196306
196802
  this.receive();
196307
196803
  }
@@ -196871,8 +197367,8 @@ class AsyncQueue {
196871
197367
  if (this.failed) {
196872
197368
  return Promise.reject(this.failure);
196873
197369
  }
196874
- return new Promise((resolve3, reject) => {
196875
- this.waiters.push({ resolve: resolve3, reject });
197370
+ return new Promise((resolve4, reject) => {
197371
+ this.waiters.push({ resolve: resolve4, reject });
196876
197372
  });
196877
197373
  }
196878
197374
  }
@@ -197558,7 +198054,7 @@ function isSeverity(value) {
197558
198054
  return typeof value === "string" && severities.includes(value);
197559
198055
  }
197560
198056
  function normalizeCisaSecureByDesign(value) {
197561
- if (!isRecord5(value))
198057
+ if (!isRecord6(value))
197562
198058
  return;
197563
198059
  const normalized = {};
197564
198060
  const customerSecurityOutcomes = normalizeGateStatus(value.customerSecurityOutcomes);
@@ -197599,7 +198095,7 @@ function normalizeFindingFiles(value) {
197599
198095
  if (!Array.isArray(value))
197600
198096
  return;
197601
198097
  const files = value.flatMap((item) => {
197602
- if (!isRecord5(item) || typeof item.path !== "string" || item.path.trim().length === 0) {
198098
+ if (!isRecord6(item) || typeof item.path !== "string" || item.path.trim().length === 0) {
197603
198099
  return [];
197604
198100
  }
197605
198101
  const file2 = {
@@ -197618,7 +198114,7 @@ function normalizeFindingFiles(value) {
197618
198114
  function normalizeLineNumber(value) {
197619
198115
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
197620
198116
  }
197621
- function isRecord5(value) {
198117
+ function isRecord6(value) {
197622
198118
  return typeof value === "object" && value !== null && !Array.isArray(value);
197623
198119
  }
197624
198120
 
@@ -197806,7 +198302,7 @@ async function readWorkspaceFile(workspaceDir, requestedPath, line, limit) {
197806
198302
  const readablePath = await resolveReadableFile(workspaceRoot, absolute);
197807
198303
  if (!readablePath)
197808
198304
  continue;
197809
- content = await readFile5(readablePath, "utf8").catch(() => {
198305
+ content = await readFile6(readablePath, "utf8").catch(() => {
197810
198306
  return;
197811
198307
  });
197812
198308
  if (content !== undefined)
@@ -197823,7 +198319,7 @@ async function readWorkspaceFile(workspaceDir, requestedPath, line, limit) {
197823
198319
  }
197824
198320
  async function resolveReadableFile(workspaceRoot, absolute) {
197825
198321
  const realPath = await realpath(absolute).catch((error51) => {
197826
- if (isMissingPathError3(error51))
198322
+ if (isMissingPathError4(error51))
197827
198323
  return;
197828
198324
  throw error51;
197829
198325
  });
@@ -197833,13 +198329,13 @@ async function resolveReadableFile(workspaceRoot, absolute) {
197833
198329
  return realPath;
197834
198330
  }
197835
198331
  function resolveReadablePaths(workspaceRoot, requestedPath) {
197836
- const primary = resolve3(workspaceRoot, requestedPath);
198332
+ const primary = resolve4(workspaceRoot, requestedPath);
197837
198333
  assertWithinWorkspace(workspaceRoot, primary);
197838
198334
  const relativePath = relative(workspaceRoot, primary).replaceAll("\\", "/");
197839
198335
  if (relativePath.startsWith("context/") || relativePath.startsWith("repo/")) {
197840
198336
  return [primary];
197841
198337
  }
197842
- const repoPath = resolve3(workspaceRoot, "repo", relativePath);
198338
+ const repoPath = resolve4(workspaceRoot, "repo", relativePath);
197843
198339
  assertWithinWorkspace(workspaceRoot, repoPath);
197844
198340
  return isAbsolute(requestedPath) ? [primary, repoPath] : [repoPath, primary];
197845
198341
  }
@@ -197861,7 +198357,7 @@ function terminateChild(child) {
197861
198357
  }, 2000);
197862
198358
  killTimer.unref();
197863
198359
  }
197864
- function isMissingPathError3(error51) {
198360
+ function isMissingPathError4(error51) {
197865
198361
  return typeof error51 === "object" && error51 !== null && "code" in error51 && error51.code === "ENOENT";
197866
198362
  }
197867
198363
  function buildAgentFailure(rawDetail, fallbackMessage) {
@@ -198010,7 +198506,7 @@ function buildOpinion(agent, role, tool) {
198010
198506
  }
198011
198507
 
198012
198508
  // src/acp/prompts.ts
198013
- function buildAgentPrompt(tool, request, agent) {
198509
+ function buildAgentPrompt(tool, request, agent, role) {
198014
198510
  const shared = [
198015
198511
  "You are running as a Kyoso child reviewer.",
198016
198512
  "Do not edit files.",
@@ -198023,15 +198519,26 @@ function buildAgentPrompt(tool, request, agent) {
198023
198519
  "Use empty arrays when no finding, test, risk, or question exists; do not copy the example finding."
198024
198520
  ].join(`
198025
198521
  `);
198026
- const role = agent === "codex" ? [
198027
- "You are the Codex implementation reviewer in Kyoso.",
198028
- "Focus on feasibility, minimal change, existing code consistency, regression risk, tests, migration risk, and maintainability."
198029
- ].join(`
198030
- `) : [
198031
- "You are the Claude architecture and security reviewer in Kyoso.",
198032
- "Focus on architecture, threat modeling, authn/authz, secrets, privacy, secure defaults, CISA Secure by Design, and edge cases."
198033
- ].join(`
198034
- `);
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
+ };
198035
198542
  const cisaInstruction = tool === "security_review" ? [
198036
198543
  "For security_review, include cisaMapping on each security-relevant finding when applicable.",
198037
198544
  "Also include cisaSecureByDesign with all four gate dimensions."
@@ -198039,7 +198546,9 @@ function buildAgentPrompt(tool, request, agent) {
198039
198546
  `) : "For plan_review and diff_review, include cisaMapping and cisaSecureByDesign only when relevant.";
198040
198547
  return `${shared}
198041
198548
 
198042
- ${role}
198549
+ Agent: ${agent}
198550
+ Role: ${role}
198551
+ ${roleInstructions[role]}
198043
198552
 
198044
198553
  Tool: ${tool}
198045
198554
  ${cisaInstruction}
@@ -198382,8 +198891,8 @@ function normalizeTitle(value) {
198382
198891
  }
198383
198892
 
198384
198893
  // src/audit/trace.ts
198385
- import { mkdir as mkdir3, appendFile } from "node:fs/promises";
198386
- import { dirname as dirname4, isAbsolute as isAbsolute2, join as join3 } from "node:path";
198894
+ import { mkdir as mkdir4, appendFile } from "node:fs/promises";
198895
+ import { dirname as dirname5, isAbsolute as isAbsolute2, join as join4 } from "node:path";
198387
198896
 
198388
198897
  // src/context/pathPolicy.ts
198389
198898
  import { normalize, sep } from "node:path";
@@ -198477,13 +198986,13 @@ function createTraceWriter(options) {
198477
198986
  }
198478
198987
  const date5 = new Date().toISOString().slice(0, 10);
198479
198988
  const directory = validateAuditDirectory(options.directory, warnings);
198480
- const tracePath = join3(options.cwd, directory, date5, `${options.traceId}.jsonl`);
198989
+ const tracePath = join4(options.cwd, directory, date5, `${options.traceId}.jsonl`);
198481
198990
  return {
198482
198991
  tracePath,
198483
198992
  warnings,
198484
198993
  async write(event) {
198485
198994
  try {
198486
- await mkdir3(dirname4(tracePath), { recursive: true });
198995
+ await mkdir4(dirname5(tracePath), { recursive: true });
198487
198996
  await appendFile(tracePath, `${JSON.stringify(sanitizeForAudit(event, {
198488
198997
  includeRawAgentOutput: options.includeRawAgentOutput
198489
198998
  }))}
@@ -198626,6 +199135,7 @@ function renderMarkdownResult(tool, result, options = {}) {
198626
199135
  `**Decision:** ${result.decision}`,
198627
199136
  `**Mode:** ${tool}`,
198628
199137
  `**Agents:** ${result.agentOpinions.map((opinion) => `${title(opinion.agent)} ${opinion.status}`).join(", ")}`,
199138
+ `**Review mode:** ${formatReviewMode(result)}`,
198629
199139
  `**Degraded:** ${String(result.degraded)}`,
198630
199140
  "",
198631
199141
  "## Summary",
@@ -198652,7 +199162,11 @@ function renderMarkdownResult(tool, result, options = {}) {
198652
199162
  lines.push(`### ${title(opinion.agent)}`, "", `${opinion.summary} (${opinion.status})`, "");
198653
199163
  }
198654
199164
  lines.push("", "## Disagreements", "");
198655
- 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
+ }
198656
199170
  lines.push("", "## Notes", "", "Kyoso did not modify files. Review was performed on a temporary snapshot.");
198657
199171
  if (result.audit.networkMode === "unrestricted") {
198658
199172
  lines.push("Network mode was unrestricted. File modification policy remained denied.");
@@ -198666,6 +199180,14 @@ function defaultSummaryText(result) {
198666
199180
  function title(value) {
198667
199181
  return value.slice(0, 1).toUpperCase() + value.slice(1);
198668
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
+ }
198669
199191
  function notes(items) {
198670
199192
  return (items[0] ?? "No notes.").replaceAll("|", "\\|");
198671
199193
  }
@@ -198866,15 +199388,15 @@ function decide(input2) {
198866
199388
  }
198867
199389
 
198868
199390
  // src/workspace/createSnapshot.ts
198869
- import { chmod, mkdir as mkdir4, mkdtemp, writeFile as writeFile4 } from "node:fs/promises";
198870
- import { dirname as dirname5, join as join4 } from "node:path";
199391
+ import { chmod, mkdir as mkdir5, mkdtemp, writeFile as writeFile5 } from "node:fs/promises";
199392
+ import { dirname as dirname6, join as join5 } from "node:path";
198871
199393
  import { tmpdir } from "node:os";
198872
199394
  async function createSnapshot(traceId, tool, request, options = {}) {
198873
- const root = await mkdtemp(join4(tmpdir(), `kyoso-${traceId}-`));
198874
- const repoDir = join4(root, "repo");
198875
- const contextDir = join4(root, "context");
198876
- await mkdir4(repoDir, { recursive: true });
198877
- await mkdir4(contextDir, { recursive: true });
199395
+ const root = await mkdtemp(join5(tmpdir(), `kyoso-${traceId}-`));
199396
+ const repoDir = join5(root, "repo");
199397
+ const contextDir = join5(root, "context");
199398
+ await mkdir5(repoDir, { recursive: true });
199399
+ await mkdir5(contextDir, { recursive: true });
198878
199400
  let fileCount = 0;
198879
199401
  for (const file2 of request.selectedFiles ?? []) {
198880
199402
  const relative2 = normalizeRelativePath(file2.path);
@@ -198882,24 +199404,24 @@ async function createSnapshot(traceId, tool, request, options = {}) {
198882
199404
  continue;
198883
199405
  if (!isAllowedPath(relative2, options.allowPatterns ?? []))
198884
199406
  continue;
198885
- const dest = join4(repoDir, relative2);
198886
- await mkdir4(dirname5(dest), { recursive: true });
198887
- await writeFile4(dest, file2.content, "utf8");
199407
+ const dest = join5(repoDir, relative2);
199408
+ await mkdir5(dirname6(dest), { recursive: true });
199409
+ await writeFile5(dest, file2.content, "utf8");
198888
199410
  await chmod(dest, 292).catch(() => {
198889
199411
  return;
198890
199412
  });
198891
199413
  fileCount += 1;
198892
199414
  }
198893
- await writeFile4(join4(contextDir, "request.json"), JSON.stringify(stripContents(request), null, 2), "utf8");
198894
- await writeFile4(join4(contextDir, "selected_files_manifest.json"), JSON.stringify(buildSelectedFilesManifest(request), null, 2), "utf8");
198895
- await writeFile4(join4(contextDir, "instructions.codex.md"), buildAgentPrompt(tool, request, "codex"), "utf8");
198896
- await writeFile4(join4(contextDir, "instructions.claude.md"), buildAgentPrompt(tool, request, "claude"), "utf8");
199415
+ await writeFile5(join5(contextDir, "request.json"), JSON.stringify(stripContents(request), null, 2), "utf8");
199416
+ await writeFile5(join5(contextDir, "selected_files_manifest.json"), JSON.stringify(buildSelectedFilesManifest(request), null, 2), "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");
198897
199419
  if (request.repoSummary)
198898
- await writeFile4(join4(contextDir, "repo_summary.md"), request.repoSummary, "utf8");
199420
+ await writeFile5(join5(contextDir, "repo_summary.md"), request.repoSummary, "utf8");
198899
199421
  if (request.currentPlan)
198900
- await writeFile4(join4(contextDir, "current_plan.md"), request.currentPlan, "utf8");
199422
+ await writeFile5(join5(contextDir, "current_plan.md"), request.currentPlan, "utf8");
198901
199423
  if (request.diff?.unifiedDiff)
198902
- await writeFile4(join4(contextDir, "diff.patch"), request.diff.unifiedDiff, "utf8");
199424
+ await writeFile5(join5(contextDir, "diff.patch"), request.diff.unifiedDiff, "utf8");
198903
199425
  return { root, repoDir, contextDir, fileCount };
198904
199426
  }
198905
199427
  function stripContents(request) {
@@ -199055,9 +199577,11 @@ async function runReview(tool, request, options = {}) {
199055
199577
  allowPatterns
199056
199578
  });
199057
199579
  warnings.push(...built.warnings);
199580
+ const agentRoles = resolveAgentRoles(loaded.config);
199058
199581
  snapshot = await createSnapshot(traceId, tool, built.request, {
199059
199582
  denyPatterns,
199060
- allowPatterns
199583
+ allowPatterns,
199584
+ agentRoles
199061
199585
  });
199062
199586
  await trace.write({
199063
199587
  type: "snapshot_created",
@@ -199077,6 +199601,8 @@ async function runReview(tool, request, options = {}) {
199077
199601
  trace
199078
199602
  });
199079
199603
  const normalizedAgentResults = agentResults.map(normalizeAgentRunResult);
199604
+ const agentsUsed = normalizedAgentResults.map((result2) => result2.agent);
199605
+ const reviewMode = agentsUsed.length === 1 ? "single_agent" : "multi_agent";
199080
199606
  const completed = normalizedAgentResults.filter((result2) => result2.status === "completed");
199081
199607
  const degraded = completed.length !== agentResults.length;
199082
199608
  let aggregate = aggregateAgentResults(normalizedAgentResults);
@@ -199128,6 +199654,8 @@ async function runReview(tool, request, options = {}) {
199128
199654
  const resultWithoutMarkdown = {
199129
199655
  decision,
199130
199656
  degraded,
199657
+ agentsUsed,
199658
+ reviewMode,
199131
199659
  findings: aggregate.findings,
199132
199660
  cisaSecureByDesign: cisa,
199133
199661
  disagreements: aggregate.disagreements,
@@ -199140,7 +199668,7 @@ async function runReview(tool, request, options = {}) {
199140
199668
  traceId,
199141
199669
  startedAt,
199142
199670
  completedAt,
199143
- agentsUsed: normalizedAgentResults.map((result2) => result2.agent),
199671
+ agentsUsed,
199144
199672
  redactionsApplied: secretScan.redactions,
199145
199673
  networkMode,
199146
199674
  workspaceMode: "temp_snapshot",
@@ -199199,12 +199727,13 @@ async function runReview(tool, request, options = {}) {
199199
199727
  }
199200
199728
  }
199201
199729
  async function runAgents(input2) {
199730
+ const agentRoles = resolveAgentRoles(input2.config);
199202
199731
  const agentInputs = ["codex", "claude"].filter((agent) => input2.config.agents[agent].enabled).map((agent) => ({
199203
199732
  traceId: input2.traceId,
199204
199733
  agent,
199205
- role: input2.config.agents[agent].role,
199734
+ role: agentRoles[agent] ?? input2.config.agents[agent].role,
199206
199735
  tool: input2.tool,
199207
- prompt: buildAgentPrompt(input2.tool, input2.request, agent),
199736
+ prompt: buildAgentPrompt(input2.tool, input2.request, agent, agentRoles[agent] ?? input2.config.agents[agent].role),
199208
199737
  workspaceDir: input2.workspaceDir,
199209
199738
  timeoutMs: input2.request.options?.maxAgentTimeoutMs ?? input2.config.agents[agent].timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS,
199210
199739
  networkMode: input2.networkMode
@@ -199213,6 +199742,7 @@ async function runAgents(input2) {
199213
199742
  type: "agent_started",
199214
199743
  traceId: input2.traceId,
199215
199744
  agent: agentInput.agent,
199745
+ role: agentInput.role,
199216
199746
  timestamp: new Date().toISOString()
199217
199747
  })));
199218
199748
  const results = await input2.manager.runAll(agentInputs);
@@ -199221,6 +199751,7 @@ async function runAgents(input2) {
199221
199751
  type: "agent_completed",
199222
199752
  traceId: input2.traceId,
199223
199753
  agent: result.agent,
199754
+ role: result.role,
199224
199755
  status: result.status,
199225
199756
  startedAt: result.startedAt,
199226
199757
  completedAt: result.completedAt,
@@ -199237,6 +199768,15 @@ async function runAgents(input2) {
199237
199768
  }));
199238
199769
  return results;
199239
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
+ }
199240
199780
  function defaultAgentManager(config2) {
199241
199781
  if (process.env.KYOSO_TEST_FAKE_AGENTS === "1")
199242
199782
  return new FakeAgentManager;
@@ -199274,6 +199814,8 @@ async function buildSecretBlockResult(input2) {
199274
199814
  const resultWithoutMarkdown = {
199275
199815
  decision: "block",
199276
199816
  degraded: false,
199817
+ agentsUsed: [],
199818
+ reviewMode: "multi_agent",
199277
199819
  findings: [finding],
199278
199820
  cisaSecureByDesign: cisa,
199279
199821
  disagreements: [],
@@ -199354,6 +199896,8 @@ async function buildPolicyBlockResult(input2) {
199354
199896
  const resultWithoutMarkdown = {
199355
199897
  decision: "block",
199356
199898
  degraded: false,
199899
+ agentsUsed: [],
199900
+ reviewMode: "multi_agent",
199357
199901
  findings: [input2.finding],
199358
199902
  cisaSecureByDesign: input2.tool === "security_review" ? computeCisaGate([input2.finding], []) : undefined,
199359
199903
  disagreements: [],
@@ -199395,7 +199939,7 @@ function mergeDenyPatterns(configDeny, requestDeny) {
199395
199939
  function assertTrustedWorkspaceRoot(requestRoot, configRoot, cwd) {
199396
199940
  if (!requestRoot)
199397
199941
  return;
199398
- if (resolve4(cwd, requestRoot) !== resolve4(cwd, configRoot)) {
199942
+ if (resolve5(cwd, requestRoot) !== resolve5(cwd, configRoot)) {
199399
199943
  throw new KyosoRequestError("workspace.root is not trusted by config", "UNTRUSTED_WORKSPACE_ROOT");
199400
199944
  }
199401
199945
  }
@@ -199503,6 +200047,17 @@ async function main() {
199503
200047
  console.log(await runInit({ cwd, force: booleanFlag(parsed.flags, "force") }));
199504
200048
  return;
199505
200049
  }
200050
+ if (parsed.command === "setup") {
200051
+ console.log(await runSetup({
200052
+ cwd,
200053
+ client: parsed.positionals[0],
200054
+ write: booleanFlag(parsed.flags, "write"),
200055
+ global: booleanFlag(parsed.flags, "global"),
200056
+ runner: stringFlag(parsed.flags, "runner"),
200057
+ command: stringFlag(parsed.flags, "command")
200058
+ }));
200059
+ return;
200060
+ }
199506
200061
  if (parsed.command === "plan" || parsed.command === "security" || parsed.command === "diff") {
199507
200062
  const tool = commandToTool(parsed.command);
199508
200063
  const request = await buildReviewRequest(tool, parsed.flags);
@@ -199554,7 +200109,7 @@ async function buildDiff(tool, flags) {
199554
200109
  return;
199555
200110
  const base = stringFlag(flags, "base") ?? "main";
199556
200111
  const head = stringFlag(flags, "head") ?? "HEAD";
199557
- const result = spawnSync("git", ["diff", base, head], { encoding: "utf8" });
200112
+ const result = spawnSync2("git", ["diff", base, head], { encoding: "utf8" });
199558
200113
  if (result.status !== 0) {
199559
200114
  throw new Error(result.stderr || `git diff failed for ${base}..${head}`);
199560
200115
  }
@@ -199594,6 +200149,7 @@ var HELP = `Kyoso
199594
200149
 
199595
200150
  Usage:
199596
200151
  kyoso mcp [--config kyoso.config.ts] [--ignore-config] [--trust-config] [--network model_only|unrestricted]
200152
+ kyoso setup [codex|claude-code] [--write] [--runner npx|bunx] [--command <command>] [--global]
199597
200153
  kyoso plan --goal <text> [--plan <path-or-text>] [--file <path>] [--json] [--trust-config]
199598
200154
  kyoso security --goal <text> [--diff <path>] [--file <path>] [--allow-secret-redaction] [--trust-config]
199599
200155
  kyoso diff --base main --head HEAD [--json] [--trust-config]