cc-codeconductor 0.3.0 → 0.3.2

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/index.js CHANGED
@@ -7044,7 +7044,7 @@ function getExitCode(error) {
7044
7044
  // package.json
7045
7045
  var package_default = {
7046
7046
  name: "cc-codeconductor",
7047
- version: "0.3.0",
7047
+ version: "0.3.2",
7048
7048
  description: "A multi-agent orchestration framework for AI-assisted software engineering workflows.",
7049
7049
  keywords: [
7050
7050
  "ai",
@@ -7511,7 +7511,8 @@ function getRecommendedPresets(profile) {
7511
7511
  }
7512
7512
 
7513
7513
  // src/commands/doctor.command.ts
7514
- import { join } from "node:path";
7514
+ import { join as join4 } from "node:path";
7515
+ import { homedir as homedir3 } from "node:os";
7515
7516
 
7516
7517
  // src/core/config/config-loader.ts
7517
7518
  import { readFile } from "node:fs/promises";
@@ -11721,251 +11722,1011 @@ async function loadTargetSecurityCompatibility() {
11721
11722
  });
11722
11723
  }
11723
11724
 
11724
- // src/commands/doctor.command.ts
11725
- async function doctorCommand(options) {
11726
- const { projectRoot, output } = options;
11727
- const checks = [];
11728
- try {
11729
- const hasConfig = await configExists(projectRoot);
11730
- if (hasConfig) {
11731
- checks.push({
11732
- name: "config-exists",
11733
- status: "pass",
11734
- message: ".codeconductor/config.yml exists"
11735
- });
11736
- } else {
11737
- checks.push({
11738
- name: "config-exists",
11739
- status: "fail",
11740
- message: ".codeconductor/config.yml not found. Run `codeconductor init` first."
11741
- });
11742
- return {
11743
- code: 4,
11744
- data: {
11745
- success: false,
11746
- command: "doctor",
11747
- checks
11748
- }
11749
- };
11750
- }
11751
- const configResult = await loadConfig(projectRoot);
11752
- if (configResult.success) {
11753
- checks.push({
11754
- name: "config-valid",
11755
- status: "pass",
11756
- message: "Config is valid"
11757
- });
11758
- } else {
11759
- checks.push({
11760
- name: "config-valid",
11761
- status: "fail",
11762
- message: `Config validation failed: ${configResult.error.message}`
11763
- });
11764
- return {
11765
- code: 1,
11766
- data: {
11767
- success: false,
11768
- command: "doctor",
11769
- checks
11770
- }
11771
- };
11772
- }
11773
- const runnerDirs = [".opencode", ".claude", ".codex"];
11774
- for (const dir of runnerDirs) {
11775
- try {
11776
- const { access: access2 } = await import("node:fs/promises");
11777
- await access2(join(projectRoot, dir));
11778
- checks.push({
11779
- name: `dir-${dir}`,
11780
- status: "pass",
11781
- message: `${dir}/ exists`
11782
- });
11783
- } catch {
11784
- checks.push({
11785
- name: `dir-${dir}`,
11786
- status: "warn",
11787
- message: `${dir}/ not found (optional)`
11788
- });
11789
- }
11790
- }
11791
- const config = configResult.data;
11792
- if (config.presets.council.enabled) {
11793
- checks.push({
11794
- name: "council-enabled",
11795
- status: "pass",
11796
- message: `Council preset enabled (v${config.presets.council.version})`
11797
- });
11798
- }
11799
- const securityCompatibility = await loadTargetSecurityCompatibility();
11800
- for (const compatibility of securityCompatibility) {
11801
- checks.push({
11802
- name: `security-${compatibility.target}`,
11803
- status: compatibility.status,
11804
- message: compatibility.status === "pass" ? `${compatibility.target} can represent the canonical policy model` : `${compatibility.target} cannot enforce: ${compatibility.unsupportedRules.join(", ") || "see warnings"}`
11805
- });
11806
- }
11807
- const failedCount = checks.filter((c) => c.status === "fail").length;
11808
- if (failedCount > 0) {
11809
- return {
11810
- code: 4,
11811
- data: {
11812
- success: false,
11813
- command: "doctor",
11814
- checks
11815
- }
11816
- };
11817
- }
11818
- return {
11819
- code: 0,
11820
- data: {
11821
- success: true,
11822
- command: "doctor",
11823
- checks,
11824
- securityCompatibility
11825
- }
11826
- };
11827
- } catch (error) {
11828
- return {
11829
- code: 1,
11830
- data: {
11831
- success: false,
11832
- command: "doctor",
11833
- errors: [String(error)]
11834
- }
11835
- };
11836
- }
11725
+ // src/core/presets/update-checker.ts
11726
+ import { stat as stat2, readFile as readFile5 } from "node:fs/promises";
11727
+ import { resolve as resolve4, join as join3 } from "node:path";
11728
+ import { homedir as homedir2 } from "node:os";
11729
+
11730
+ // src/core/presets/manifest-loader.ts
11731
+ import { readFile as readFile3 } from "node:fs/promises";
11732
+ import { join } from "node:path";
11733
+ var MANIFESTS_DIR = join(SRC_PRESETS_DIR, "manifests");
11734
+ var MODELS_DIR = join(SRC_PRESETS_DIR, "models");
11735
+ var PRESETS_DIR = ROOT_PRESETS_DIR;
11736
+ async function loadManifest(target) {
11737
+ const manifestPath = join(MANIFESTS_DIR, `${target}.yml`);
11738
+ const content = await readFile3(manifestPath, "utf-8");
11739
+ const data = $parse(content);
11740
+ return InstallManifestSchema.parse(data);
11741
+ }
11742
+ async function loadModelConfig(target) {
11743
+ const modelPath = join(MODELS_DIR, `${target}.yml`);
11744
+ const content = await readFile3(modelPath, "utf-8");
11745
+ const data = $parse(content);
11746
+ return ModelConfigSchema.parse(data);
11837
11747
  }
11838
11748
 
11839
- // src/commands/init.command.ts
11840
- import { access as access3, mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
11749
+ // src/core/presets/file-copier.ts
11750
+ import { mkdir, readdir, readFile as readFile4, stat, writeFile } from "node:fs/promises";
11841
11751
  import { homedir } from "node:os";
11842
- import { basename, resolve as resolve4 } from "node:path";
11843
-
11844
- // src/core/config/config-writer.ts
11845
- import { access as access2, mkdir, writeFile } from "node:fs/promises";
11846
- import { resolve as resolve3 } from "node:path";
11752
+ import { dirname as dirname2, join as join2, relative, resolve as resolve3 } from "node:path";
11847
11753
 
11848
- // src/core/config/codeconductor-config.ts
11849
- var DEFAULT_CONFIG = {
11850
- version: "0.2.0",
11851
- project: {
11852
- name: "unnamed-project"
11853
- },
11854
- defaults: {
11855
- target: "opencode",
11856
- overwrite: false,
11857
- locale: "en"
11858
- },
11859
- presets: {
11860
- council: {
11861
- enabled: true,
11862
- version: "0.1.0"
11863
- }
11864
- },
11865
- safety: {
11866
- destructiveCommands: ["rm -rf", "drop table", "delete from"],
11867
- secretPatterns: ["password", "secret", "api_key", "token"]
11868
- }
11754
+ // src/core/i18n/language-instructions.ts
11755
+ var SUPPORTED_LOCALES = ["en", "es"];
11756
+ var LANGUAGE_INSTRUCTIONS = {
11757
+ en: 'Prose/docs/code comments: be terse and direct. Prefer concrete nouns over abstract ones. Omit filler phrases ("note that", "please", "as mentioned"). One idea per sentence.',
11758
+ es: "Spanish prose/docs/reports/Markdown: preserve natural Spanish orthography, including accents, `ñ`, `¿`, `¡`, and normal Unicode. The ASCII-only editing preference does not apply to these artifacts."
11869
11759
  };
11760
+ var COMMIT_STYLE = {
11761
+ en: `---
11762
+ trigger: always_on
11763
+ description: Rules for generating commit messages using Conventional Commits format
11764
+ ---
11870
11765
 
11871
- // src/core/config/config-writer.ts
11872
- var CONFIG_DIR = ".codeconductor";
11873
- var CONFIG_FILE2 = "config.yml";
11874
- async function writeConfig(projectRoot, config = {}, force = false) {
11875
- try {
11876
- const configDir = resolve3(projectRoot, CONFIG_DIR);
11877
- await mkdir(configDir, { recursive: true });
11878
- const configPath = resolve3(configDir, CONFIG_FILE2);
11879
- if (!force) {
11880
- try {
11881
- await access2(configPath);
11882
- return ok(undefined);
11883
- } catch {}
11884
- }
11885
- const mergedConfig = {
11886
- ...DEFAULT_CONFIG,
11887
- ...config,
11888
- project: {
11889
- ...DEFAULT_CONFIG.project,
11890
- ...config.project
11891
- },
11892
- defaults: {
11893
- ...DEFAULT_CONFIG.defaults,
11894
- ...config.defaults
11895
- },
11896
- presets: {
11897
- ...DEFAULT_CONFIG.presets,
11898
- ...config.presets
11899
- },
11900
- safety: {
11901
- ...DEFAULT_CONFIG.safety,
11902
- ...config.safety
11903
- }
11904
- };
11905
- const yamlContent = $stringify(mergedConfig);
11906
- await writeFile(configPath, yamlContent, "utf-8");
11907
- return ok(undefined);
11908
- } catch (error) {
11909
- return err(new ValidationError("Failed to write config", error));
11910
- }
11911
- }
11766
+ ## Git Commit Messages
11912
11767
 
11913
- // src/core/presets/preset-resolver.ts
11914
- var CURRENT_PRESET_VERSION = "v0.3.0";
11915
- function resolvePreset(target, profile) {
11916
- const stack = resolveStack(profile);
11917
- const isMonorepo = profile.signals.some((s) => [
11918
- "pnpm-workspace.yaml",
11919
- "lerna.json",
11920
- "go.work",
11921
- "package.json-workspaces",
11922
- "Cargo.toml-workspace"
11923
- ].includes(s));
11924
- const architecture = isMonorepo ? "monorepo" : profile.signals.length > 0 ? "single-project" : "unknown";
11925
- const warnings = [];
11926
- if (profile.confidence === "low") {
11927
- warnings.push("Detection confidence is low; review the selected preset before applying it.");
11928
- }
11929
- if (stack === "unknown") {
11930
- warnings.push("No supported stack was detected; using the generic CodeConductor workflow preset.");
11931
- } else {
11932
- warnings.push(`${stack} projects currently receive the generic ${target} workflow plus matching skills; stack-specific asset pruning is not implemented yet.`);
11933
- }
11934
- if (architecture === "unknown") {
11935
- warnings.push("Project architecture could not be inferred from available signals.");
11936
- }
11937
- return {
11938
- target,
11939
- stack,
11940
- architecture,
11941
- confidence: profile.confidence,
11942
- presetVersion: CURRENT_PRESET_VERSION,
11943
- assets: resolveAssets(target),
11944
- warnings
11945
- };
11946
- }
11947
- function resolveStack(profile) {
11948
- if (profile.frameworks.includes("spring"))
11949
- return "spring";
11950
- if (profile.frameworks.includes("django"))
11951
- return "django";
11952
- if (profile.frameworks.includes("astro"))
11953
- return "astro";
11954
- if (profile.frameworks.includes("nextjs"))
11955
- return "nextjs";
11956
- if (profile.frameworks.includes("fastapi"))
11957
- return "fastapi";
11958
- if (profile.frameworks.includes("backend"))
11959
- return "backend";
11960
- if (profile.frameworks.includes("frontend"))
11961
- return "frontend";
11962
- if (profile.frameworks.includes("android"))
11963
- return "android";
11964
- if (profile.frameworks.includes("laravel"))
11965
- return "laravel";
11966
- if (profile.runtimes.includes("php"))
11967
- return "php";
11968
- if (profile.runtimes.includes("bun"))
11768
+ ### Format
11769
+
11770
+ \`\`\`
11771
+ <type>(<scope>): <short description>
11772
+
11773
+ - <detail 1>
11774
+ - <detail 2>
11775
+ \`\`\`
11776
+
11777
+ ### Valid Types
11778
+
11779
+ \`feat\` \`fix\` \`docs\` \`style\` \`refactor\` \`test\` \`chore\` \`perf\` \`ci\` \`build\`
11780
+ \`revert\`
11781
+
11782
+ ### Rules
11783
+
11784
+ - Language: **neutral English** always
11785
+ - Header: maximum 69 characters, no trailing period
11786
+ - Body: concise bullet points, one idea per line
11787
+ - Footer: only for breaking changes or issues
11788
+ - No gerunds ("adding", "fixing")
11789
+ - Use infinitive or imperative ("add", "fix", "update")`,
11790
+ es: `---
11791
+ trigger: always_on
11792
+ description: Reglas para generar mensajes de commit con formato Conventional Commits
11793
+ ---
11794
+
11795
+ ## Git Commit Messages
11796
+
11797
+ ### Formato
11798
+
11799
+ \`\`\`
11800
+ <tipo>(<scope>): <descripcion corta>
11801
+
11802
+ - <detalle 1>
11803
+ - <detalle 2>
11804
+ \`\`\`
11805
+
11806
+ ### Tipos validos
11807
+
11808
+ \`feat\` \`fix\` \`docs\` \`style\` \`refactor\` \`test\` \`chore\` \`perf\` \`ci\` \`build\`
11809
+ \`revert\`
11810
+
11811
+ ### Reglas
11812
+
11813
+ - Idioma: **inglés neutro** siempre
11814
+ - Encabezado: maximo 69 caracteres, sin punto final
11815
+ - Cuerpo: viñetas concisas, una idea por linea
11816
+ - Footer: solo para breaking changes o issues
11817
+ - Sin gerundios ("agregando", "corrigiendo")
11818
+ - Usar infinitivo o imperativo ("agregar", "corregir", "actualizar")`
11819
+ };
11820
+ var COMMIT_WORKFLOW = {
11821
+ en: `---
11822
+ name: commit
11823
+ description: Generates a commit in English following Conventional Commits based on staged changes
11824
+ ---
11825
+
11826
+ // turbo-all
11827
+
11828
+ ## Steps
11829
+
11830
+ 1. View staged changes: \`git diff --cached --stat\`
11831
+ 2. If nothing is staged, suggest \`git add .\` or specific files
11832
+ 3. Get full diff: \`git diff --cached\`
11833
+ 4. Check recent style: \`git log -n 3 --oneline\`
11834
+ 5. Generate message following \`.agents/rules/commit-style.md\`
11835
+ 6. Show proposal and confirm with the user
11836
+ 7. Execute: \`git commit -m "<message>"\`
11837
+ 8. Confirm success: \`git status\``,
11838
+ es: `---
11839
+ name: commit
11840
+ description: Genera un commit en inglés siguiendo Conventional Commits basado en los cambios staged
11841
+ ---
11842
+
11843
+ // turbo-all
11844
+
11845
+ ## Pasos
11846
+
11847
+ 1. Ver cambios staged: \`git diff --cached --stat\`
11848
+ 2. Si no hay nada staged, sugiere \`git add .\` o archivos específicos
11849
+ 3. Obtener diff completo: \`git diff --cached\`
11850
+ 4. Ver estilo reciente: \`git log -n 3 --oneline\`
11851
+ 5. Generar mensaje siguiendo \`.agents/rules/commit-style.md\`
11852
+ 6. Mostrar propuesta y confirmar con el usuario
11853
+ 7. Ejecutar: \`git commit -m "<mensaje>"\`
11854
+ 8. Confirmar éxito: \`git status\``
11855
+ };
11856
+ function getLanguageInstruction(locale) {
11857
+ const key = SUPPORTED_LOCALES.includes(locale) ? locale : "en";
11858
+ return `- ${LANGUAGE_INSTRUCTIONS[key]}`;
11859
+ }
11860
+ var LOCALE_PLACEHOLDER = "{{LANGUAGE_INSTRUCTIONS}}";
11861
+
11862
+ // src/core/filesystem/safe-merger.ts
11863
+ var MANAGED_BEGIN_MARKER = "<!-- CODECONDUCTOR:BEGIN managed -->";
11864
+ var MANAGED_END_MARKER = "<!-- CODECONDUCTOR:END managed -->";
11865
+ function mergeManagedBlock(existing, incoming) {
11866
+ validateMarkers(incoming, "incoming content");
11867
+ if (!existing) {
11868
+ return { content: incoming, action: "written" };
11869
+ }
11870
+ const hasBegin = existing.includes(MANAGED_BEGIN_MARKER);
11871
+ const hasEnd = existing.includes(MANAGED_END_MARKER);
11872
+ if (!hasBegin && !hasEnd) {
11873
+ return { content: incoming, action: "written" };
11874
+ }
11875
+ validateMarkers(existing, "existing content");
11876
+ const existingBlock = getManagedBlockRange(existing);
11877
+ const incomingBlock = getManagedBlockRange(incoming);
11878
+ return {
11879
+ content: existing.slice(0, existingBlock.start) + incoming.slice(incomingBlock.start, incomingBlock.end) + existing.slice(existingBlock.end),
11880
+ action: "merged"
11881
+ };
11882
+ }
11883
+ function validateMarkers(content, label) {
11884
+ const beginCount = countOccurrences(content, MANAGED_BEGIN_MARKER);
11885
+ const endCount = countOccurrences(content, MANAGED_END_MARKER);
11886
+ if (beginCount !== 1 || endCount !== 1) {
11887
+ throw new Error(`${label} must contain exactly one managed begin marker and one managed end marker`);
11888
+ }
11889
+ if (content.indexOf(MANAGED_BEGIN_MARKER) > content.indexOf(MANAGED_END_MARKER)) {
11890
+ throw new Error(`${label} has managed markers in the wrong order`);
11891
+ }
11892
+ }
11893
+ function getManagedBlockRange(content) {
11894
+ const start = content.indexOf(MANAGED_BEGIN_MARKER);
11895
+ const end = content.indexOf(MANAGED_END_MARKER) + MANAGED_END_MARKER.length;
11896
+ return { start, end };
11897
+ }
11898
+ function countOccurrences(content, needle) {
11899
+ return content.split(needle).length - 1;
11900
+ }
11901
+
11902
+ // src/core/presets/file-copier.ts
11903
+ async function listFilesRecursive(dir, base = dir) {
11904
+ const entries = await readdir(dir, { withFileTypes: true });
11905
+ const files = [];
11906
+ for (const entry of entries) {
11907
+ const full = join2(dir, entry.name);
11908
+ if (entry.isDirectory()) {
11909
+ files.push(...await listFilesRecursive(full, base));
11910
+ } else {
11911
+ files.push(relative(base, full));
11912
+ }
11913
+ }
11914
+ return files;
11915
+ }
11916
+ async function resolveEntryFiles(entry, presetsDir, baseDir) {
11917
+ const srcAbsolute = resolve3(presetsDir, entry.src);
11918
+ try {
11919
+ const s = await stat(srcAbsolute);
11920
+ if (s.isDirectory()) {
11921
+ const files = await listFilesRecursive(srcAbsolute);
11922
+ return files.map((f) => ({
11923
+ src: join2(srcAbsolute, f),
11924
+ dest: resolve3(baseDir, entry.dest, f)
11925
+ }));
11926
+ }
11927
+ return [{ src: srcAbsolute, dest: resolve3(baseDir, entry.dest) }];
11928
+ } catch {
11929
+ return [];
11930
+ }
11931
+ }
11932
+ function mergeDeep(target, source) {
11933
+ const result = { ...target };
11934
+ for (const key of Object.keys(source)) {
11935
+ const srcVal = source[key];
11936
+ const tgtVal = target[key];
11937
+ if (Array.isArray(srcVal) && Array.isArray(tgtVal)) {
11938
+ result[key] = [...new Set([...tgtVal, ...srcVal])];
11939
+ } else if (srcVal && typeof srcVal === "object" && !Array.isArray(srcVal) && tgtVal && typeof tgtVal === "object" && !Array.isArray(tgtVal)) {
11940
+ result[key] = mergeDeep(tgtVal, srcVal);
11941
+ } else {
11942
+ result[key] = srcVal;
11943
+ }
11944
+ }
11945
+ return result;
11946
+ }
11947
+ function extractAgentRole(filePath) {
11948
+ const parts = filePath.replace(/\\/g, "/").split("/");
11949
+ const basename = parts[parts.length - 1];
11950
+ const name = basename.replace(/\.md$/, "");
11951
+ const knownRoles = [
11952
+ "architect",
11953
+ "implementer",
11954
+ "tester",
11955
+ "orchestrator",
11956
+ "reviewer",
11957
+ "docs",
11958
+ "task-coach",
11959
+ "repo-explorer"
11960
+ ];
11961
+ return knownRoles.includes(name) ? name : null;
11962
+ }
11963
+ function renderTemplate(content, modelConfig, filePath, locale = "en") {
11964
+ const cleanLocale = locale === "es" || locale === "en" ? locale : "en";
11965
+ const templatedContent = content.replace(/\{\{COMMIT_STYLE\}\}/g, COMMIT_STYLE[cleanLocale]).replace(/\{\{COMMIT_WORKFLOW\}\}/g, COMMIT_WORKFLOW[cleanLocale]);
11966
+ const agentRole = extractAgentRole(filePath);
11967
+ if (agentRole && modelConfig.agents[agentRole]) {
11968
+ const agentModels = modelConfig.agents[agentRole];
11969
+ const targetModel = agentModels[modelConfig.target];
11970
+ let result2 = templatedContent.replace(/\{\{MODEL\}\}/g, targetModel ?? "").replace(/\{\{MODEL_CLAUDE\}\}/g, agentModels.claude ?? "").replace(/\{\{MODEL_OPENCODE\}\}/g, agentModels.opencode ?? "").replace(/\{\{MODEL_CODEX\}\}/g, agentModels.codex ?? "").replace(/\{\{MODEL_GEMINI\}\}/g, agentModels.gemini ?? "").replace(/\{\{MODEL_CURSOR\}\}/g, agentModels.cursor ?? "").replace(new RegExp(LOCALE_PLACEHOLDER.replace(/[{}]/g, "\\$&"), "g"), getLanguageInstruction(locale));
11971
+ if (modelConfig.tools || modelConfig.permissions) {
11972
+ result2 = substituteToolNames(result2, modelConfig);
11973
+ }
11974
+ return result2;
11975
+ }
11976
+ const knownRoles = [
11977
+ "orchestrator",
11978
+ "task-coach",
11979
+ "architect",
11980
+ "implementer",
11981
+ "tester",
11982
+ "reviewer",
11983
+ "docs",
11984
+ "repo-explorer"
11985
+ ];
11986
+ let result = templatedContent;
11987
+ for (const role of knownRoles) {
11988
+ if (!modelConfig.agents[role])
11989
+ continue;
11990
+ const agentModels = modelConfig.agents[role];
11991
+ const sectionRegex = new RegExp(`(### ${role}[\\s\\S]*?)(?=### |## |$)`, "gi");
11992
+ const sectionMatch = result.match(sectionRegex);
11993
+ if (sectionMatch) {
11994
+ for (const section of sectionMatch) {
11995
+ const renderedSection = section.replace(/\{\{MODEL_CLAUDE\}\}/g, agentModels.claude ?? "").replace(/\{\{MODEL_OPENCODE\}\}/g, agentModels.opencode ?? "").replace(/\{\{MODEL_CODEX\}\}/g, agentModels.codex ?? "").replace(/\{\{MODEL_GEMINI\}\}/g, agentModels.gemini ?? "").replace(/\{\{MODEL_CURSOR\}\}/g, agentModels.cursor ?? "");
11996
+ result = result.replace(section, renderedSection);
11997
+ }
11998
+ }
11999
+ }
12000
+ result = result.replace(new RegExp(LOCALE_PLACEHOLDER.replace(/[{}]/g, "\\$&"), "g"), getLanguageInstruction(locale));
12001
+ if (modelConfig.tools || modelConfig.permissions) {
12002
+ result = substituteToolNames(result, modelConfig);
12003
+ }
12004
+ return result;
12005
+ }
12006
+ function substituteToolNames(content, modelConfig) {
12007
+ if (!modelConfig.tools && !modelConfig.permissions)
12008
+ return content;
12009
+ const target = modelConfig.target;
12010
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
12011
+ if (!fmMatch)
12012
+ return content;
12013
+ const frontmatter = fmMatch[1];
12014
+ if (target === "opencode" && modelConfig.permissions) {
12015
+ const updatedFrontmatter2 = frontmatter.replace(/^tools:\s*(.+)\r?\n?/m, "");
12016
+ return content.replace(fmMatch[0], `---
12017
+ ${updatedFrontmatter2}
12018
+ ---`);
12019
+ }
12020
+ if (!modelConfig.tools)
12021
+ return content;
12022
+ const updatedFrontmatter = frontmatter.replace(/^tools:\s*(.+)$/m, (_match, toolsLine) => {
12023
+ const baseNames = toolsLine.split(",").map((t) => t.trim());
12024
+ const mappedNames = baseNames.map((baseName) => {
12025
+ const toolMapping = modelConfig.tools?.[baseName];
12026
+ if (toolMapping && toolMapping[target]) {
12027
+ return toolMapping[target];
12028
+ }
12029
+ return baseName;
12030
+ });
12031
+ return `tools: ${mappedNames.join(", ")}`;
12032
+ });
12033
+ return content.replace(fmMatch[0], `---
12034
+ ${updatedFrontmatter}
12035
+ ---`);
12036
+ }
12037
+ async function applySingleFile(srcPath, destPath, strategy, force, dryRun, isTemplate, modelConfig, locale) {
12038
+ if (strategy === "skip") {
12039
+ return { src: srcPath, dest: destPath, action: "skipped", dryRun };
12040
+ }
12041
+ let content;
12042
+ try {
12043
+ content = await readFile4(srcPath, "utf-8");
12044
+ } catch (e) {
12045
+ return { src: srcPath, dest: destPath, action: "error", error: `Cannot read source: ${e}` };
12046
+ }
12047
+ const incomingContent = isTemplate && modelConfig ? renderTemplate(content, modelConfig, srcPath, locale) : content;
12048
+ let finalContent = incomingContent;
12049
+ let action = "written";
12050
+ if (strategy === "append") {
12051
+ let existing = "";
12052
+ try {
12053
+ existing = await readFile4(destPath, "utf-8");
12054
+ } catch {}
12055
+ if (existing) {
12056
+ finalContent = existing + `
12057
+
12058
+ ---
12059
+
12060
+ ` + incomingContent;
12061
+ }
12062
+ action = "appended";
12063
+ } else if (strategy === "merge-json") {
12064
+ let existing = {};
12065
+ try {
12066
+ existing = JSON.parse(await readFile4(destPath, "utf-8"));
12067
+ } catch {}
12068
+ try {
12069
+ const incoming = JSON.parse(incomingContent);
12070
+ finalContent = JSON.stringify(mergeDeep(existing, incoming), null, 2);
12071
+ } catch (e) {
12072
+ return { src: srcPath, dest: destPath, action: "error", error: `JSON merge failed: ${e}` };
12073
+ }
12074
+ action = "merged";
12075
+ } else if (strategy === "merge-managed") {
12076
+ let existing = null;
12077
+ try {
12078
+ existing = await readFile4(destPath, "utf-8");
12079
+ } catch {}
12080
+ try {
12081
+ const merged = mergeManagedBlock(existing, incomingContent);
12082
+ finalContent = merged.content;
12083
+ action = merged.action;
12084
+ } catch (e) {
12085
+ return { src: srcPath, dest: destPath, action: "error", error: `Managed merge failed: ${e}` };
12086
+ }
12087
+ }
12088
+ if (dryRun) {
12089
+ return { src: srcPath, dest: destPath, action, dryRun: true };
12090
+ }
12091
+ try {
12092
+ await mkdir(dirname2(destPath), { recursive: true });
12093
+ await writeFile(destPath, finalContent, "utf-8");
12094
+ return { src: srcPath, dest: destPath, action };
12095
+ } catch (e) {
12096
+ return { src: srcPath, dest: destPath, action: "error", error: String(e) };
12097
+ }
12098
+ }
12099
+ async function copyFromManifest(manifest, presetsDir, baseDir, isGlobal, dryRun, force, modelConfig = null, locale = "en") {
12100
+ const results = [];
12101
+ for (const entry of manifest.entries) {
12102
+ const strategy = isGlobal && entry.globalStrategy ? entry.globalStrategy : entry.strategy;
12103
+ let resolvedBaseDir = baseDir;
12104
+ let resolvedEntry = entry;
12105
+ if (manifest.target === "agy" && isGlobal) {
12106
+ resolvedBaseDir = join2(homedir(), ".gemini", "config");
12107
+ resolvedEntry = {
12108
+ ...entry,
12109
+ dest: entry.dest.replace(/^\.agents\/?/, "")
12110
+ };
12111
+ }
12112
+ const files = await resolveEntryFiles(resolvedEntry, presetsDir, resolvedBaseDir);
12113
+ const isTemplate = entry.template === true;
12114
+ for (const { src, dest } of files) {
12115
+ results.push(await applySingleFile(src, dest, strategy, force, dryRun, isTemplate, modelConfig, locale));
12116
+ }
12117
+ }
12118
+ return results;
12119
+ }
12120
+
12121
+ // src/core/presets/update-checker.ts
12122
+ function getTargetInstallationPath(target, basePath, isGlobal) {
12123
+ if (target === "agy") {
12124
+ return isGlobal ? resolve4(basePath, ".gemini", "config") : resolve4(basePath, ".agents");
12125
+ }
12126
+ return resolve4(basePath, `.${target}`);
12127
+ }
12128
+ async function isTargetInstalled(target, basePath, isGlobal) {
12129
+ const path = getTargetInstallationPath(target, basePath, isGlobal);
12130
+ try {
12131
+ const s = await stat2(path);
12132
+ return s.isDirectory();
12133
+ } catch {
12134
+ return false;
12135
+ }
12136
+ }
12137
+ async function validateAgentFileSizes(basePath, isGlobal) {
12138
+ const filesToCheck = [];
12139
+ filesToCheck.push(resolve4(basePath, ".claude", "CLAUDE.md"));
12140
+ filesToCheck.push(resolve4(basePath, ".codex", "AGENTS.md"));
12141
+ if (isGlobal) {
12142
+ filesToCheck.push(resolve4(basePath, ".gemini", "config", "AGENTS.md"));
12143
+ } else {
12144
+ filesToCheck.push(resolve4(basePath, ".agents", "AGENTS.md"));
12145
+ }
12146
+ const largeFiles = [];
12147
+ for (const filePath of filesToCheck) {
12148
+ try {
12149
+ const s = await stat2(filePath);
12150
+ if (s.isFile() && s.size > 40 * 1024) {
12151
+ largeFiles.push({ path: filePath, size: s.size });
12152
+ }
12153
+ } catch {}
12154
+ }
12155
+ return largeFiles;
12156
+ }
12157
+ async function validateAgentMarkers(basePath, isGlobal) {
12158
+ const targetsToCheck = [
12159
+ "opencode",
12160
+ "claude",
12161
+ "codex",
12162
+ "gemini",
12163
+ "cursor",
12164
+ "agy"
12165
+ ];
12166
+ const results = [];
12167
+ for (const target of targetsToCheck) {
12168
+ const isInstalled = await isTargetInstalled(target, basePath, isGlobal);
12169
+ if (!isInstalled) {
12170
+ continue;
12171
+ }
12172
+ let manifest;
12173
+ try {
12174
+ manifest = await loadManifest(target);
12175
+ } catch {
12176
+ continue;
12177
+ }
12178
+ for (const entry of manifest.entries) {
12179
+ const strategy = isGlobal && entry.globalStrategy ? entry.globalStrategy : entry.strategy;
12180
+ if (strategy !== "merge-managed") {
12181
+ continue;
12182
+ }
12183
+ let resolvedEntry = entry;
12184
+ let targetBaseDir = basePath;
12185
+ if (target === "agy" && isGlobal) {
12186
+ targetBaseDir = join3(homedir2(), ".gemini", "config");
12187
+ resolvedEntry = {
12188
+ ...entry,
12189
+ dest: entry.dest.replace(/^\.agents\/?/, "")
12190
+ };
12191
+ }
12192
+ const files = await resolveEntryFiles(resolvedEntry, PRESETS_DIR, targetBaseDir);
12193
+ for (const { dest } of files) {
12194
+ try {
12195
+ const content = await readFile5(dest, "utf-8");
12196
+ const beginCount = content.split(MANAGED_BEGIN_MARKER).length - 1;
12197
+ const endCount = content.split(MANAGED_END_MARKER).length - 1;
12198
+ if (beginCount === 0 && endCount === 0) {
12199
+ results.push({ path: dest, error: "Missing managed markers" });
12200
+ } else if (beginCount !== 1 || endCount !== 1) {
12201
+ results.push({
12202
+ path: dest,
12203
+ error: "Must contain exactly one managed begin marker and one managed end marker"
12204
+ });
12205
+ } else if (content.indexOf(MANAGED_BEGIN_MARKER) > content.indexOf(MANAGED_END_MARKER)) {
12206
+ results.push({
12207
+ path: dest,
12208
+ error: "Managed markers are in the wrong order"
12209
+ });
12210
+ }
12211
+ } catch {}
12212
+ }
12213
+ }
12214
+ }
12215
+ return results;
12216
+ }
12217
+ async function fileContentDiffers(pathA, pathB) {
12218
+ try {
12219
+ const contentA = await readFile5(pathA, "utf-8");
12220
+ const contentB = await readFile5(pathB, "utf-8");
12221
+ return contentA.trim() !== contentB.trim();
12222
+ } catch {
12223
+ try {
12224
+ await stat2(pathB);
12225
+ return true;
12226
+ } catch {
12227
+ return false;
12228
+ }
12229
+ }
12230
+ }
12231
+ async function loadSkillsLock(basePath) {
12232
+ const paths = [
12233
+ resolve4(basePath, ".codeconductor", "skills-lock.json"),
12234
+ resolve4(basePath, ".agents", "skills-lock.json")
12235
+ ];
12236
+ for (const p of paths) {
12237
+ try {
12238
+ const content = await readFile5(p, "utf-8");
12239
+ return JSON.parse(content);
12240
+ } catch {}
12241
+ }
12242
+ return null;
12243
+ }
12244
+ async function getLatestSkillVersion(skillId) {
12245
+ const targets = [
12246
+ "opencode",
12247
+ "agy",
12248
+ "claude",
12249
+ "codex",
12250
+ "gemini",
12251
+ "cursor"
12252
+ ];
12253
+ for (const target of targets) {
12254
+ const skillPath = resolve4(ROOT_PRESETS_DIR, target, "skills", skillId, "SKILL.md");
12255
+ try {
12256
+ const content = await readFile5(skillPath, "utf-8");
12257
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
12258
+ if (fmMatch) {
12259
+ const parsed = $parse(fmMatch[1]);
12260
+ if (parsed && parsed.id === skillId && parsed.version) {
12261
+ return String(parsed.version);
12262
+ }
12263
+ }
12264
+ } catch {}
12265
+ }
12266
+ return null;
12267
+ }
12268
+ async function checkUpdates(basePath, isGlobal) {
12269
+ const localCouncil = resolve4(basePath, ".codeconductor", "presets", "council.yml");
12270
+ const bundledCouncil = resolve4(SRC_PRESETS_DIR, "council", "council.yml");
12271
+ const councilHasUpdate = await fileContentDiffers(localCouncil, bundledCouncil);
12272
+ const localPolicy = resolve4(basePath, ".codeconductor", "presets", "policy.yml");
12273
+ const policyHasUpdate = await fileContentDiffers(localPolicy, POLICY_PATH);
12274
+ const targetsToCheck = [
12275
+ "opencode",
12276
+ "claude",
12277
+ "codex",
12278
+ "gemini",
12279
+ "cursor",
12280
+ "agy"
12281
+ ];
12282
+ const targetResults = [];
12283
+ let locale = "en";
12284
+ try {
12285
+ const configResult = await loadConfig(basePath);
12286
+ if (configResult.success) {
12287
+ locale = configResult.data.defaults.locale ?? "en";
12288
+ }
12289
+ } catch {}
12290
+ for (const target of targetsToCheck) {
12291
+ const isInstalled = await isTargetInstalled(target, basePath, isGlobal);
12292
+ if (!isInstalled) {
12293
+ continue;
12294
+ }
12295
+ let manifest;
12296
+ let modelConfig;
12297
+ try {
12298
+ manifest = await loadManifest(target);
12299
+ modelConfig = await loadModelConfig(target);
12300
+ } catch {
12301
+ continue;
12302
+ }
12303
+ const changedFiles = [];
12304
+ for (const entry of manifest.entries) {
12305
+ const strategy = isGlobal && entry.globalStrategy ? entry.globalStrategy : entry.strategy;
12306
+ if (strategy === "skip")
12307
+ continue;
12308
+ let resolvedEntry = entry;
12309
+ let targetBaseDir = basePath;
12310
+ if (target === "agy" && isGlobal) {
12311
+ targetBaseDir = join3(homedir2(), ".gemini", "config");
12312
+ resolvedEntry = {
12313
+ ...entry,
12314
+ dest: entry.dest.replace(/^\.agents\/?/, "")
12315
+ };
12316
+ }
12317
+ const files = await resolveEntryFiles(resolvedEntry, PRESETS_DIR, targetBaseDir);
12318
+ const isTemplate = entry.template === true;
12319
+ for (const { src, dest } of files) {
12320
+ let expectedContent;
12321
+ try {
12322
+ const srcContent = await readFile5(src, "utf-8");
12323
+ expectedContent = isTemplate && modelConfig ? renderTemplate(srcContent, modelConfig, src, locale) : srcContent;
12324
+ } catch {
12325
+ continue;
12326
+ }
12327
+ let destContent;
12328
+ try {
12329
+ destContent = await readFile5(dest, "utf-8");
12330
+ } catch {
12331
+ changedFiles.push(dest);
12332
+ continue;
12333
+ }
12334
+ let fileHasUpdate = false;
12335
+ if (strategy === "overwrite") {
12336
+ if (destContent.trim() !== expectedContent.trim()) {
12337
+ fileHasUpdate = true;
12338
+ }
12339
+ } else if (strategy === "append") {
12340
+ if (!destContent.includes(expectedContent)) {
12341
+ fileHasUpdate = true;
12342
+ }
12343
+ } else if (strategy === "merge-json") {
12344
+ try {
12345
+ const destJson = JSON.parse(destContent);
12346
+ const expectedJson = JSON.parse(expectedContent);
12347
+ const merged = mergeDeep(destJson, expectedJson);
12348
+ if (JSON.stringify(merged) !== JSON.stringify(destJson)) {
12349
+ fileHasUpdate = true;
12350
+ }
12351
+ } catch {
12352
+ fileHasUpdate = true;
12353
+ }
12354
+ } else if (strategy === "merge-managed") {
12355
+ try {
12356
+ const merged = mergeManagedBlock(destContent, expectedContent);
12357
+ if (merged.content.trim() !== destContent.trim()) {
12358
+ fileHasUpdate = true;
12359
+ }
12360
+ } catch {
12361
+ fileHasUpdate = true;
12362
+ }
12363
+ }
12364
+ if (fileHasUpdate) {
12365
+ changedFiles.push(dest);
12366
+ }
12367
+ }
12368
+ }
12369
+ targetResults.push({
12370
+ target,
12371
+ hasUpdate: changedFiles.length > 0,
12372
+ files: changedFiles
12373
+ });
12374
+ }
12375
+ const skillResults = [];
12376
+ const skillsLock = await loadSkillsLock(basePath);
12377
+ if (skillsLock) {
12378
+ for (const [skillId, currentVersion] of Object.entries(skillsLock)) {
12379
+ const latestVersion = await getLatestSkillVersion(skillId);
12380
+ if (latestVersion && latestVersion !== currentVersion) {
12381
+ skillResults.push({
12382
+ id: skillId,
12383
+ currentVersion,
12384
+ latestVersion,
12385
+ hasUpdate: true
12386
+ });
12387
+ } else {
12388
+ skillResults.push({
12389
+ id: skillId,
12390
+ currentVersion,
12391
+ latestVersion: latestVersion || currentVersion,
12392
+ hasUpdate: false
12393
+ });
12394
+ }
12395
+ }
12396
+ }
12397
+ const hasPresetUpdates = councilHasUpdate || policyHasUpdate;
12398
+ const hasTargetUpdates = targetResults.some((t) => t.hasUpdate);
12399
+ const hasSkillUpdates = skillResults.some((s) => s.hasUpdate);
12400
+ return {
12401
+ hasUpdates: hasPresetUpdates || hasTargetUpdates || hasSkillUpdates,
12402
+ council: councilHasUpdate,
12403
+ policy: policyHasUpdate,
12404
+ targets: targetResults,
12405
+ skills: skillResults
12406
+ };
12407
+ }
12408
+
12409
+ // src/commands/doctor.command.ts
12410
+ async function doctorCommand(options) {
12411
+ const { projectRoot, output } = options;
12412
+ const checks = [];
12413
+ try {
12414
+ const hasConfig = await configExists(projectRoot);
12415
+ if (hasConfig) {
12416
+ checks.push({
12417
+ name: "config-exists",
12418
+ status: "pass",
12419
+ message: ".codeconductor/config.yml exists"
12420
+ });
12421
+ } else {
12422
+ checks.push({
12423
+ name: "config-exists",
12424
+ status: "fail",
12425
+ message: ".codeconductor/config.yml not found. Run `codeconductor init` first."
12426
+ });
12427
+ return {
12428
+ code: 4,
12429
+ data: {
12430
+ success: false,
12431
+ command: "doctor",
12432
+ checks
12433
+ }
12434
+ };
12435
+ }
12436
+ const configResult = await loadConfig(projectRoot);
12437
+ if (configResult.success) {
12438
+ checks.push({
12439
+ name: "config-valid",
12440
+ status: "pass",
12441
+ message: "Config is valid"
12442
+ });
12443
+ } else {
12444
+ checks.push({
12445
+ name: "config-valid",
12446
+ status: "fail",
12447
+ message: `Config validation failed: ${configResult.error.message}`
12448
+ });
12449
+ return {
12450
+ code: 1,
12451
+ data: {
12452
+ success: false,
12453
+ command: "doctor",
12454
+ checks
12455
+ }
12456
+ };
12457
+ }
12458
+ const runnerDirs = [".opencode", ".claude", ".codex"];
12459
+ for (const dir of runnerDirs) {
12460
+ try {
12461
+ const { access: access2 } = await import("node:fs/promises");
12462
+ await access2(join4(projectRoot, dir));
12463
+ checks.push({
12464
+ name: `dir-${dir}`,
12465
+ status: "pass",
12466
+ message: `${dir}/ exists`
12467
+ });
12468
+ } catch {
12469
+ checks.push({
12470
+ name: `dir-${dir}`,
12471
+ status: "warn",
12472
+ message: `${dir}/ not found (optional)`
12473
+ });
12474
+ }
12475
+ }
12476
+ const config = configResult.data;
12477
+ if (config.presets.council.enabled) {
12478
+ checks.push({
12479
+ name: "council-enabled",
12480
+ status: "pass",
12481
+ message: `Council preset enabled (v${config.presets.council.version})`
12482
+ });
12483
+ }
12484
+ const localUpdates = await checkUpdates(projectRoot, false);
12485
+ const globalUpdates = await checkUpdates(homedir3(), true);
12486
+ const updateDetails = [];
12487
+ if (localUpdates.hasUpdates) {
12488
+ if (localUpdates.council)
12489
+ updateDetails.push("local council preset");
12490
+ if (localUpdates.policy)
12491
+ updateDetails.push("local policy");
12492
+ const updatedLocalTargets = localUpdates.targets.filter((t) => t.hasUpdate).map((t) => t.target);
12493
+ if (updatedLocalTargets.length > 0) {
12494
+ updateDetails.push(`local targets (${updatedLocalTargets.join(", ")})`);
12495
+ }
12496
+ const updatedLocalSkills = localUpdates.skills.filter((s) => s.hasUpdate).map((s) => s.id);
12497
+ if (updatedLocalSkills.length > 0) {
12498
+ updateDetails.push(`local skills (${updatedLocalSkills.join(", ")})`);
12499
+ }
12500
+ }
12501
+ if (globalUpdates.hasUpdates) {
12502
+ if (globalUpdates.council)
12503
+ updateDetails.push("global council preset");
12504
+ if (globalUpdates.policy)
12505
+ updateDetails.push("global policy");
12506
+ const updatedGlobalTargets = globalUpdates.targets.filter((t) => t.hasUpdate).map((t) => t.target);
12507
+ if (updatedGlobalTargets.length > 0) {
12508
+ updateDetails.push(`global targets (${updatedGlobalTargets.join(", ")})`);
12509
+ }
12510
+ const updatedGlobalSkills = globalUpdates.skills.filter((s) => s.hasUpdate).map((s) => s.id);
12511
+ if (updatedGlobalSkills.length > 0) {
12512
+ updateDetails.push(`global skills (${updatedGlobalSkills.join(", ")})`);
12513
+ }
12514
+ }
12515
+ if (updateDetails.length > 0) {
12516
+ checks.push({
12517
+ name: "updates-available",
12518
+ status: "warn",
12519
+ message: `Updates available for: ${updateDetails.join(", ")}`
12520
+ });
12521
+ } else {
12522
+ checks.push({
12523
+ name: "updates-available",
12524
+ status: "pass",
12525
+ message: "All presets, targets, and skills are up to date"
12526
+ });
12527
+ }
12528
+ const largeFilesLocal = await validateAgentFileSizes(projectRoot, false);
12529
+ const largeFilesGlobal = await validateAgentFileSizes(homedir3(), true);
12530
+ const allLargeFiles = [...largeFilesLocal, ...largeFilesGlobal];
12531
+ if (allLargeFiles.length > 0) {
12532
+ checks.push({
12533
+ name: "agent-file-sizes",
12534
+ status: "warn",
12535
+ message: `The following files exceed 40KB: ${allLargeFiles.map((f) => f.path).join(", ")}`
12536
+ });
12537
+ } else {
12538
+ checks.push({
12539
+ name: "agent-file-sizes",
12540
+ status: "pass",
12541
+ message: "All agent files (AGENTS.md/CLAUDE.md) are under 40KB"
12542
+ });
12543
+ }
12544
+ const missingMarkersLocal = await validateAgentMarkers(projectRoot, false);
12545
+ const missingMarkersGlobal = await validateAgentMarkers(homedir3(), true);
12546
+ const allMissingMarkers = [...missingMarkersLocal, ...missingMarkersGlobal];
12547
+ if (allMissingMarkers.length > 0) {
12548
+ checks.push({
12549
+ name: "agent-file-markers",
12550
+ status: "warn",
12551
+ message: `The following files have missing or invalid managed markers: ${allMissingMarkers.map((f) => `${f.path} (${f.error})`).join(", ")}`
12552
+ });
12553
+ } else {
12554
+ checks.push({
12555
+ name: "agent-file-markers",
12556
+ status: "pass",
12557
+ message: "All agent files have valid managed markers"
12558
+ });
12559
+ }
12560
+ const securityCompatibility = await loadTargetSecurityCompatibility();
12561
+ for (const compatibility of securityCompatibility) {
12562
+ checks.push({
12563
+ name: `security-${compatibility.target}`,
12564
+ status: compatibility.status,
12565
+ message: compatibility.status === "pass" ? `${compatibility.target} can represent the canonical policy model` : `${compatibility.target} cannot enforce: ${compatibility.unsupportedRules.join(", ") || "see warnings"}`
12566
+ });
12567
+ }
12568
+ const failedCount = checks.filter((c) => c.status === "fail").length;
12569
+ if (failedCount > 0) {
12570
+ return {
12571
+ code: 4,
12572
+ data: {
12573
+ success: false,
12574
+ command: "doctor",
12575
+ checks
12576
+ }
12577
+ };
12578
+ }
12579
+ return {
12580
+ code: 0,
12581
+ data: {
12582
+ success: true,
12583
+ command: "doctor",
12584
+ checks,
12585
+ securityCompatibility
12586
+ }
12587
+ };
12588
+ } catch (error) {
12589
+ return {
12590
+ code: 1,
12591
+ data: {
12592
+ success: false,
12593
+ command: "doctor",
12594
+ errors: [String(error)]
12595
+ }
12596
+ };
12597
+ }
12598
+ }
12599
+
12600
+ // src/commands/init.command.ts
12601
+ import { access as access3, mkdir as mkdir3, readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
12602
+ import { homedir as homedir4 } from "node:os";
12603
+ import { basename, resolve as resolve6 } from "node:path";
12604
+
12605
+ // src/core/config/config-writer.ts
12606
+ import { access as access2, mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
12607
+ import { resolve as resolve5 } from "node:path";
12608
+
12609
+ // src/core/config/codeconductor-config.ts
12610
+ var DEFAULT_CONFIG = {
12611
+ version: "0.2.0",
12612
+ project: {
12613
+ name: "unnamed-project"
12614
+ },
12615
+ defaults: {
12616
+ target: "opencode",
12617
+ overwrite: false,
12618
+ locale: "en"
12619
+ },
12620
+ presets: {
12621
+ council: {
12622
+ enabled: true,
12623
+ version: "0.1.0"
12624
+ }
12625
+ },
12626
+ safety: {
12627
+ destructiveCommands: ["rm -rf", "drop table", "delete from"],
12628
+ secretPatterns: ["password", "secret", "api_key", "token"]
12629
+ }
12630
+ };
12631
+
12632
+ // src/core/config/config-writer.ts
12633
+ var CONFIG_DIR = ".codeconductor";
12634
+ var CONFIG_FILE2 = "config.yml";
12635
+ async function writeConfig(projectRoot, config = {}, force = false) {
12636
+ try {
12637
+ const configDir = resolve5(projectRoot, CONFIG_DIR);
12638
+ await mkdir2(configDir, { recursive: true });
12639
+ const configPath = resolve5(configDir, CONFIG_FILE2);
12640
+ if (!force) {
12641
+ try {
12642
+ await access2(configPath);
12643
+ return ok(undefined);
12644
+ } catch {}
12645
+ }
12646
+ const mergedConfig = {
12647
+ ...DEFAULT_CONFIG,
12648
+ ...config,
12649
+ project: {
12650
+ ...DEFAULT_CONFIG.project,
12651
+ ...config.project
12652
+ },
12653
+ defaults: {
12654
+ ...DEFAULT_CONFIG.defaults,
12655
+ ...config.defaults
12656
+ },
12657
+ presets: {
12658
+ ...DEFAULT_CONFIG.presets,
12659
+ ...config.presets
12660
+ },
12661
+ safety: {
12662
+ ...DEFAULT_CONFIG.safety,
12663
+ ...config.safety
12664
+ }
12665
+ };
12666
+ const yamlContent = $stringify(mergedConfig);
12667
+ await writeFile2(configPath, yamlContent, "utf-8");
12668
+ return ok(undefined);
12669
+ } catch (error) {
12670
+ return err(new ValidationError("Failed to write config", error));
12671
+ }
12672
+ }
12673
+
12674
+ // src/core/presets/preset-resolver.ts
12675
+ var CURRENT_PRESET_VERSION = "v0.3.0";
12676
+ function resolvePreset(target, profile) {
12677
+ const stack = resolveStack(profile);
12678
+ const isMonorepo = profile.signals.some((s) => [
12679
+ "pnpm-workspace.yaml",
12680
+ "lerna.json",
12681
+ "go.work",
12682
+ "package.json-workspaces",
12683
+ "Cargo.toml-workspace"
12684
+ ].includes(s));
12685
+ const architecture = isMonorepo ? "monorepo" : profile.signals.length > 0 ? "single-project" : "unknown";
12686
+ const warnings = [];
12687
+ if (profile.confidence === "low") {
12688
+ warnings.push("Detection confidence is low; review the selected preset before applying it.");
12689
+ }
12690
+ if (stack === "unknown") {
12691
+ warnings.push("No supported stack was detected; using the generic CodeConductor workflow preset.");
12692
+ } else {
12693
+ warnings.push(`${stack} projects currently receive the generic ${target} workflow plus matching skills; stack-specific asset pruning is not implemented yet.`);
12694
+ }
12695
+ if (architecture === "unknown") {
12696
+ warnings.push("Project architecture could not be inferred from available signals.");
12697
+ }
12698
+ return {
12699
+ target,
12700
+ stack,
12701
+ architecture,
12702
+ confidence: profile.confidence,
12703
+ presetVersion: CURRENT_PRESET_VERSION,
12704
+ assets: resolveAssets(target),
12705
+ warnings
12706
+ };
12707
+ }
12708
+ function resolveStack(profile) {
12709
+ if (profile.frameworks.includes("spring"))
12710
+ return "spring";
12711
+ if (profile.frameworks.includes("django"))
12712
+ return "django";
12713
+ if (profile.frameworks.includes("astro"))
12714
+ return "astro";
12715
+ if (profile.frameworks.includes("nextjs"))
12716
+ return "nextjs";
12717
+ if (profile.frameworks.includes("fastapi"))
12718
+ return "fastapi";
12719
+ if (profile.frameworks.includes("backend"))
12720
+ return "backend";
12721
+ if (profile.frameworks.includes("frontend"))
12722
+ return "frontend";
12723
+ if (profile.frameworks.includes("android"))
12724
+ return "android";
12725
+ if (profile.frameworks.includes("laravel"))
12726
+ return "laravel";
12727
+ if (profile.runtimes.includes("php"))
12728
+ return "php";
12729
+ if (profile.runtimes.includes("bun"))
11969
12730
  return "bun";
11970
12731
  if (profile.runtimes.includes("node"))
11971
12732
  return "node";
@@ -11991,7 +12752,7 @@ function resolveAssets(target) {
11991
12752
  // src/commands/init.command.ts
11992
12753
  async function initCommand(options) {
11993
12754
  const { projectRoot, dryRun, force, global: isGlobal, output, locale } = options;
11994
- const baseDir = isGlobal ? homedir() : projectRoot;
12755
+ const baseDir = isGlobal ? homedir4() : projectRoot;
11995
12756
  try {
11996
12757
  let profile = null;
11997
12758
  if (!isGlobal) {
@@ -12098,7 +12859,7 @@ async function initCommand(options) {
12098
12859
  }
12099
12860
  async function resolvePresetsToCopy() {
12100
12861
  const sources = [];
12101
- const bundledCouncil = resolve4(SRC_PRESETS_DIR, "council", "council.yml");
12862
+ const bundledCouncil = resolve6(SRC_PRESETS_DIR, "council", "council.yml");
12102
12863
  if (await fileExists2(bundledCouncil)) {
12103
12864
  sources.push({ name: "council.yml", sourcePath: bundledCouncil });
12104
12865
  }
@@ -12109,17 +12870,17 @@ async function resolvePresetsToCopy() {
12109
12870
  return sources;
12110
12871
  }
12111
12872
  async function copyPresets(baseDir, presets, force) {
12112
- const presetsDir = resolve4(baseDir, ".codeconductor", "presets");
12113
- await mkdir2(presetsDir, { recursive: true });
12873
+ const presetsDir = resolve6(baseDir, ".codeconductor", "presets");
12874
+ await mkdir3(presetsDir, { recursive: true });
12114
12875
  const copied = [];
12115
12876
  for (const preset of presets) {
12116
- const destPath = resolve4(presetsDir, preset.name);
12877
+ const destPath = resolve6(presetsDir, preset.name);
12117
12878
  if (!force && await fileExists2(destPath)) {
12118
12879
  continue;
12119
12880
  }
12120
12881
  try {
12121
- const content = await readFile3(preset.sourcePath, "utf-8");
12122
- await writeFile2(destPath, content, "utf-8");
12882
+ const content = await readFile6(preset.sourcePath, "utf-8");
12883
+ await writeFile3(destPath, content, "utf-8");
12123
12884
  copied.push(`.codeconductor/presets/${preset.name}`);
12124
12885
  } catch (err2) {
12125
12886
  const code = err2.code;
@@ -12141,8 +12902,8 @@ async function fileExists2(path) {
12141
12902
  }
12142
12903
 
12143
12904
  // src/commands/install.command.ts
12144
- import { homedir as homedir3 } from "node:os";
12145
- import { resolve as resolve7 } from "node:path";
12905
+ import { homedir as homedir5 } from "node:os";
12906
+ import { resolve as resolve8 } from "node:path";
12146
12907
 
12147
12908
  // src/domain/council/council-agent.ts
12148
12909
  function getResponsibilities(agentId) {
@@ -12200,281 +12961,63 @@ function generateAgentContent(agent) {
12200
12961
  ## Role
12201
12962
  ${agent.role}
12202
12963
 
12203
- ## Context
12204
- ${agent.context === "repo-readonly" ? "Can read repository but cannot modify files" : "Only receives prompts, no direct repository access"}
12205
-
12206
- ## Model Hint
12207
- ${agent.modelHint}
12208
-
12209
- ## Focus Areas
12210
- ${agent.focus.map((f) => `- ${f}`).join(`
12211
- `)}
12212
-
12213
- ## Responsibilities
12214
- ${getResponsibilities(agent.id)}
12215
- `;
12216
- }
12217
-
12218
- // src/adapters/claude/claude-council-generator.ts
12219
- function generateClaudeFiles(spec) {
12220
- const files = [];
12221
- files.push({
12222
- path: ".claude/skills/council/SKILL.md",
12223
- content: generateCouncilSkill(spec),
12224
- overwrite: false
12225
- });
12226
- for (const agent of spec.agents) {
12227
- files.push({
12228
- path: `.claude/agents/council-${agent.id}.md`,
12229
- content: generateAgentContent(agent),
12230
- overwrite: false
12231
- });
12232
- }
12233
- return files;
12234
- }
12235
- function generateCouncilSkill(spec) {
12236
- return `# Council Skill
12237
-
12238
- ## Description
12239
- ${spec.description}
12240
-
12241
- ## Version
12242
- ${spec.version}
12243
-
12244
- ## Agents
12245
- ${spec.agents.map((a) => `- **${a.role}** (${a.id}): ${a.focus.join(", ")}`).join(`
12246
- `)}
12247
-
12248
- ## Usage
12249
- Use the council agents to get multi-perspective analysis on code changes, architecture decisions, and security reviews.
12250
-
12251
- ## Context
12252
- ${spec.outputContract}
12253
- `;
12254
- }
12255
-
12256
- // src/adapters/claude/claude-installer.ts
12257
- class ClaudeInstaller {
12258
- name = "claude";
12259
- target = "claude";
12260
- spec = null;
12261
- setSpec(spec) {
12262
- this.spec = spec;
12263
- }
12264
- async generate() {
12265
- if (!this.spec) {
12266
- throw new Error("Council spec not set");
12267
- }
12268
- return generateClaudeFiles(this.spec);
12269
- }
12270
- async isAvailable() {
12271
- return true;
12272
- }
12273
- }
12274
- function createClaudeInstaller(spec) {
12275
- const installer = new ClaudeInstaller;
12276
- installer.setSpec(spec);
12277
- return installer;
12278
- }
12279
-
12280
- // src/adapters/codex/codex-council-generator.ts
12281
- function generateCodexFiles(spec) {
12282
- const files = [];
12283
- files.push({
12284
- path: ".codex/config.toml",
12285
- content: generateCodexConfig(spec),
12286
- overwrite: false
12287
- });
12288
- for (const agent of spec.agents) {
12289
- files.push({
12290
- path: `.codex/agents/council_${agent.id}.toml`,
12291
- content: generateCodexAgent(agent),
12292
- overwrite: true
12293
- });
12294
- }
12295
- files.push({
12296
- path: ".codex/skills/council/SKILL.md",
12297
- content: generateCodexSkill(spec),
12298
- overwrite: true
12299
- });
12300
- return files;
12301
- }
12302
- function generateCodexConfig(spec) {
12303
- const agentTables = spec.agents.map((agent) => {
12304
- const focusAreas = agent.focus.join(", ");
12305
- return `
12306
- [agents.${agent.id}]
12307
- description = "${agent.role} council agent. Focus: ${focusAreas}. Context: ${agent.context}. Model hint: ${agent.modelHint}."
12308
- nickname_candidates = ["${agent.role}", "Council ${agent.role}"]`;
12309
- }).join(`
12310
- `);
12311
- return `# Codex Council Configuration
12312
-
12313
- [project]
12314
- name = "council"
12315
- version = "${spec.version}"
12316
- ${agentTables}
12317
- `;
12318
- }
12319
- function generateCodexAgent(agent) {
12320
- const focusAreas = agent.focus.join(", ");
12321
- return `name = "${agent.role}"
12322
- description = "${agent.role} council agent. Focus: ${focusAreas}. Context: ${agent.context}. Model hint: ${agent.modelHint}."
12323
- nickname_candidates = ["${agent.role}", "Council ${agent.role}"]
12324
- developer_instructions = "You are the ${agent.role} council agent. Your focus areas are: ${focusAreas}. Context: ${agent.context}. Apply ${agent.modelHint} reasoning to your analysis."
12325
- `;
12326
- }
12327
- function generateCodexSkill(spec) {
12328
- return `---
12329
- name: council
12330
- description: ${spec.description}
12331
- version: ${spec.version}
12332
- ---
12333
-
12334
- ## Agents
12335
- ${spec.agents.map((a) => `- **${a.role}** (${a.id}): ${a.focus.join(", ")}`).join(`
12336
- `)}
12337
-
12338
- ## Usage
12339
- Use the council agents to get multi-perspective analysis on code changes, architecture decisions, and security reviews.
12340
- `;
12341
- }
12342
-
12343
- // src/adapters/codex/codex-installer.ts
12344
- class CodexInstaller {
12345
- name = "codex";
12346
- target = "codex";
12347
- spec = null;
12348
- setSpec(spec) {
12349
- this.spec = spec;
12350
- }
12351
- async generate() {
12352
- if (!this.spec) {
12353
- throw new Error("Council spec not set");
12354
- }
12355
- return generateCodexFiles(this.spec);
12356
- }
12357
- async isAvailable() {
12358
- return true;
12359
- }
12360
- }
12361
- function createCodexInstaller(spec) {
12362
- const installer = new CodexInstaller;
12363
- installer.setSpec(spec);
12364
- return installer;
12365
- }
12366
-
12367
- // src/adapters/opencode/opencode-council-generator.ts
12368
- function generateOpenCodeFiles(spec) {
12369
- const files = [];
12370
- files.push({
12371
- path: ".opencode/commands/cc-council.md",
12372
- content: generateCouncilCommand(spec),
12373
- overwrite: false
12374
- });
12375
- files.push({
12376
- path: ".opencode/agents/council-lead.md",
12377
- content: generateCouncilLead(spec),
12378
- overwrite: false
12379
- });
12380
- for (const agent of spec.agents) {
12381
- files.push({
12382
- path: `.opencode/agents/council-${agent.id}.md`,
12383
- content: generateOpenCodeAgentContent(agent),
12384
- overwrite: false
12385
- });
12386
- }
12387
- return files;
12388
- }
12389
- function generateCouncilCommand(spec) {
12390
- return `---
12391
- description: ${yamlString(spec.description)}
12392
- agent: council-lead
12393
- subtask: true
12394
- ---
12395
-
12396
- Run the CodeConductor council for multi-perspective analysis.
12397
-
12398
- ## Version
12399
- ${spec.version}
12400
-
12401
- ## Agents
12402
- ${spec.agents.map((a) => `- ${a.role} (${a.id})`).join(`
12403
- `)}
12404
-
12405
- ## Instructions
12406
- Coordinate with the council agents and synthesize their perspectives into the configured output contract.
12407
- `;
12408
- }
12409
- function generateCouncilLead(spec) {
12410
- return `---
12411
- description: ${yamlString(`${spec.description} Council lead. Coordinates council members and synthesizes recommendations.`)}
12412
- mode: subagent
12413
- permission:
12414
- read: allow
12415
- edit: deny
12416
- bash: deny
12417
- glob: allow
12418
- grep: allow
12419
- webfetch: deny
12420
- websearch: deny
12421
- ---
12422
-
12423
- # Council Lead Agent
12424
-
12425
- ## Role
12426
- Coordinates the council and synthesizes perspectives
12964
+ ## Context
12965
+ ${agent.context === "repo-readonly" ? "Can read repository but cannot modify files" : "Only receives prompts, no direct repository access"}
12427
12966
 
12428
- ## Version
12429
- ${spec.version}
12967
+ ## Model Hint
12968
+ ${agent.modelHint}
12430
12969
 
12431
- ## Council Members
12432
- ${spec.agents.map((a) => `- ${a.role}: ${a.focus.join(", ")}`).join(`
12970
+ ## Focus Areas
12971
+ ${agent.focus.map((f) => `- ${f}`).join(`
12433
12972
  `)}
12434
12973
 
12435
12974
  ## Responsibilities
12436
- - Coordinate agent responses
12437
- - Synthesize different perspectives
12438
- - Identify consensus and disagreements
12439
- - Provide final recommendation
12975
+ ${getResponsibilities(agent.id)}
12440
12976
  `;
12441
12977
  }
12442
- function generateOpenCodeAgentContent(agent) {
12443
- return `---
12444
- description: ${yamlString(`${agent.role} council agent. Focus: ${agent.focus.join(", ")}. Context: ${agent.context}. Model hint: ${agent.modelHint}.`)}
12445
- mode: subagent
12446
- permission:
12447
- ${generatePermissionBlock(agent.context)}
12448
- ---
12449
12978
 
12450
- ${generateAgentContent(agent)}`;
12451
- }
12452
- function generatePermissionBlock(context) {
12453
- if (context === "repo-readonly") {
12454
- return ` read: allow
12455
- edit: deny
12456
- bash: deny
12457
- glob: allow
12458
- grep: allow
12459
- webfetch: deny
12460
- websearch: deny`;
12979
+ // src/adapters/claude/claude-council-generator.ts
12980
+ function generateClaudeFiles(spec) {
12981
+ const files = [];
12982
+ files.push({
12983
+ path: ".claude/skills/council/SKILL.md",
12984
+ content: generateCouncilSkill(spec),
12985
+ overwrite: false
12986
+ });
12987
+ for (const agent of spec.agents) {
12988
+ files.push({
12989
+ path: `.claude/agents/council-${agent.id}.md`,
12990
+ content: generateAgentContent(agent),
12991
+ overwrite: false
12992
+ });
12461
12993
  }
12462
- return ` read: deny
12463
- edit: deny
12464
- bash: deny
12465
- glob: deny
12466
- grep: deny
12467
- webfetch: deny
12468
- websearch: deny`;
12994
+ return files;
12469
12995
  }
12470
- function yamlString(value) {
12471
- return JSON.stringify(value);
12996
+ function generateCouncilSkill(spec) {
12997
+ return `# Council Skill
12998
+
12999
+ ## Description
13000
+ ${spec.description}
13001
+
13002
+ ## Version
13003
+ ${spec.version}
13004
+
13005
+ ## Agents
13006
+ ${spec.agents.map((a) => `- **${a.role}** (${a.id}): ${a.focus.join(", ")}`).join(`
13007
+ `)}
13008
+
13009
+ ## Usage
13010
+ Use the council agents to get multi-perspective analysis on code changes, architecture decisions, and security reviews.
13011
+
13012
+ ## Context
13013
+ ${spec.outputContract}
13014
+ `;
12472
13015
  }
12473
13016
 
12474
- // src/adapters/opencode/opencode-installer.ts
12475
- class OpenCodeInstaller {
12476
- name = "opencode";
12477
- target = "opencode";
13017
+ // src/adapters/claude/claude-installer.ts
13018
+ class ClaudeInstaller {
13019
+ name = "claude";
13020
+ target = "claude";
12478
13021
  spec = null;
12479
13022
  setSpec(spec) {
12480
13023
  this.spec = spec;
@@ -12483,467 +13026,299 @@ class OpenCodeInstaller {
12483
13026
  if (!this.spec) {
12484
13027
  throw new Error("Council spec not set");
12485
13028
  }
12486
- return generateOpenCodeFiles(this.spec);
13029
+ return generateClaudeFiles(this.spec);
12487
13030
  }
12488
13031
  async isAvailable() {
12489
13032
  return true;
12490
13033
  }
12491
13034
  }
12492
- function createOpenCodeInstaller(spec) {
12493
- const installer = new OpenCodeInstaller;
13035
+ function createClaudeInstaller(spec) {
13036
+ const installer = new ClaudeInstaller;
12494
13037
  installer.setSpec(spec);
12495
13038
  return installer;
12496
13039
  }
12497
13040
 
12498
- // src/core/filesystem/file-writer.ts
12499
- import { access as access4, mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
12500
- import { dirname as dirname2 } from "node:path";
12501
- init_safety();
12502
- async function writeGeneratedFiles(files, options) {
12503
- const results = [];
12504
- for (const file of files) {
12505
- if (!validateWritePath(file.path)) {
12506
- results.push({
12507
- path: file.path,
12508
- success: false,
12509
- error: "Protected path"
12510
- });
12511
- continue;
12512
- }
12513
- if (options.dryRun) {
12514
- results.push({
12515
- path: file.path,
12516
- success: true
12517
- });
12518
- continue;
12519
- }
12520
- if (!options.force) {
12521
- try {
12522
- await access4(file.path);
12523
- results.push({
12524
- path: file.path,
12525
- success: false,
12526
- error: "File exists, use --force to overwrite"
12527
- });
12528
- continue;
12529
- } catch {}
12530
- }
12531
- try {
12532
- const dir = dirname2(file.path);
12533
- await mkdir3(dir, { recursive: true });
12534
- await writeFile3(file.path, file.content, "utf-8");
12535
- results.push({
12536
- path: file.path,
12537
- success: true
12538
- });
12539
- } catch (error) {
12540
- results.push({
12541
- path: file.path,
12542
- success: false,
12543
- error: String(error)
12544
- });
12545
- }
13041
+ // src/adapters/codex/codex-council-generator.ts
13042
+ function generateCodexFiles(spec) {
13043
+ const files = [];
13044
+ files.push({
13045
+ path: ".codex/config.toml",
13046
+ content: generateCodexConfig(spec),
13047
+ overwrite: false
13048
+ });
13049
+ for (const agent of spec.agents) {
13050
+ files.push({
13051
+ path: `.codex/agents/council_${agent.id}.toml`,
13052
+ content: generateCodexAgent(agent),
13053
+ overwrite: true
13054
+ });
12546
13055
  }
12547
- return results;
13056
+ files.push({
13057
+ path: ".codex/skills/council/SKILL.md",
13058
+ content: generateCodexSkill(spec),
13059
+ overwrite: true
13060
+ });
13061
+ return files;
12548
13062
  }
13063
+ function generateCodexConfig(spec) {
13064
+ const agentTables = spec.agents.map((agent) => {
13065
+ const focusAreas = agent.focus.join(", ");
13066
+ return `
13067
+ [agents.${agent.id}]
13068
+ description = "${agent.role} council agent. Focus: ${focusAreas}. Context: ${agent.context}. Model hint: ${agent.modelHint}."
13069
+ nickname_candidates = ["${agent.role}", "Council ${agent.role}"]`;
13070
+ }).join(`
13071
+ `);
13072
+ return `# Codex Council Configuration
12549
13073
 
12550
- // src/core/presets/file-copier.ts
12551
- import { mkdir as mkdir4, readdir, readFile as readFile4, stat, writeFile as writeFile4 } from "node:fs/promises";
12552
- import { homedir as homedir2 } from "node:os";
12553
- import { dirname as dirname3, join as join2, relative, resolve as resolve5 } from "node:path";
12554
-
12555
- // src/core/i18n/language-instructions.ts
12556
- var SUPPORTED_LOCALES = ["en", "es"];
12557
- var LANGUAGE_INSTRUCTIONS = {
12558
- en: 'Prose/docs/code comments: be terse and direct. Prefer concrete nouns over abstract ones. Omit filler phrases ("note that", "please", "as mentioned"). One idea per sentence.',
12559
- es: "Spanish prose/docs/reports/Markdown: preserve natural Spanish orthography, including accents, `ñ`, `¿`, `¡`, and normal Unicode. The ASCII-only editing preference does not apply to these artifacts."
12560
- };
12561
- var COMMIT_STYLE = {
12562
- en: `---
12563
- trigger: always_on
12564
- description: Rules for generating commit messages using Conventional Commits format
12565
- ---
12566
-
12567
- ## Git Commit Messages
12568
-
12569
- ### Format
12570
-
12571
- \`\`\`
12572
- <type>(<scope>): <short description>
12573
-
12574
- - <detail 1>
12575
- - <detail 2>
12576
- \`\`\`
12577
-
12578
- ### Valid Types
12579
-
12580
- \`feat\` \`fix\` \`docs\` \`style\` \`refactor\` \`test\` \`chore\` \`perf\` \`ci\` \`build\`
12581
- \`revert\`
12582
-
12583
- ### Rules
12584
-
12585
- - Language: **neutral English** always
12586
- - Header: maximum 69 characters, no trailing period
12587
- - Body: concise bullet points, one idea per line
12588
- - Footer: only for breaking changes or issues
12589
- - No gerunds ("adding", "fixing")
12590
- - Use infinitive or imperative ("add", "fix", "update")`,
12591
- es: `---
12592
- trigger: always_on
12593
- description: Reglas para generar mensajes de commit con formato Conventional Commits
12594
- ---
12595
-
12596
- ## Git Commit Messages
12597
-
12598
- ### Formato
12599
-
12600
- \`\`\`
12601
- <tipo>(<scope>): <descripcion corta>
12602
-
12603
- - <detalle 1>
12604
- - <detalle 2>
12605
- \`\`\`
12606
-
12607
- ### Tipos validos
12608
-
12609
- \`feat\` \`fix\` \`docs\` \`style\` \`refactor\` \`test\` \`chore\` \`perf\` \`ci\` \`build\`
12610
- \`revert\`
12611
-
12612
- ### Reglas
12613
-
12614
- - Idioma: **inglés neutro** siempre
12615
- - Encabezado: maximo 69 caracteres, sin punto final
12616
- - Cuerpo: viñetas concisas, una idea por linea
12617
- - Footer: solo para breaking changes o issues
12618
- - Sin gerundios ("agregando", "corrigiendo")
12619
- - Usar infinitivo o imperativo ("agregar", "corregir", "actualizar")`
12620
- };
12621
- var COMMIT_WORKFLOW = {
12622
- en: `---
12623
- name: commit
12624
- description: Generates a commit in English following Conventional Commits based on staged changes
12625
- ---
12626
-
12627
- // turbo-all
12628
-
12629
- ## Steps
12630
-
12631
- 1. View staged changes: \`git diff --cached --stat\`
12632
- 2. If nothing is staged, suggest \`git add .\` or specific files
12633
- 3. Get full diff: \`git diff --cached\`
12634
- 4. Check recent style: \`git log -n 3 --oneline\`
12635
- 5. Generate message following \`.agents/rules/commit-style.md\`
12636
- 6. Show proposal and confirm with the user
12637
- 7. Execute: \`git commit -m "<message>"\`
12638
- 8. Confirm success: \`git status\``,
12639
- es: `---
12640
- name: commit
12641
- description: Genera un commit en inglés siguiendo Conventional Commits basado en los cambios staged
12642
- ---
12643
-
12644
- // turbo-all
13074
+ [project]
13075
+ name = "council"
13076
+ version = "${spec.version}"
13077
+ ${agentTables}
13078
+ `;
13079
+ }
13080
+ function generateCodexAgent(agent) {
13081
+ const focusAreas = agent.focus.join(", ");
13082
+ return `name = "${agent.role}"
13083
+ description = "${agent.role} council agent. Focus: ${focusAreas}. Context: ${agent.context}. Model hint: ${agent.modelHint}."
13084
+ nickname_candidates = ["${agent.role}", "Council ${agent.role}"]
13085
+ developer_instructions = "You are the ${agent.role} council agent. Your focus areas are: ${focusAreas}. Context: ${agent.context}. Apply ${agent.modelHint} reasoning to your analysis."
13086
+ `;
13087
+ }
13088
+ function generateCodexSkill(spec) {
13089
+ return `---
13090
+ name: council
13091
+ description: ${spec.description}
13092
+ version: ${spec.version}
13093
+ ---
12645
13094
 
12646
- ## Pasos
13095
+ ## Agents
13096
+ ${spec.agents.map((a) => `- **${a.role}** (${a.id}): ${a.focus.join(", ")}`).join(`
13097
+ `)}
12647
13098
 
12648
- 1. Ver cambios staged: \`git diff --cached --stat\`
12649
- 2. Si no hay nada staged, sugiere \`git add .\` o archivos específicos
12650
- 3. Obtener diff completo: \`git diff --cached\`
12651
- 4. Ver estilo reciente: \`git log -n 3 --oneline\`
12652
- 5. Generar mensaje siguiendo \`.agents/rules/commit-style.md\`
12653
- 6. Mostrar propuesta y confirmar con el usuario
12654
- 7. Ejecutar: \`git commit -m "<mensaje>"\`
12655
- 8. Confirmar éxito: \`git status\``
12656
- };
12657
- function getLanguageInstruction(locale) {
12658
- const key = SUPPORTED_LOCALES.includes(locale) ? locale : "en";
12659
- return `- ${LANGUAGE_INSTRUCTIONS[key]}`;
13099
+ ## Usage
13100
+ Use the council agents to get multi-perspective analysis on code changes, architecture decisions, and security reviews.
13101
+ `;
12660
13102
  }
12661
- var LOCALE_PLACEHOLDER = "{{LANGUAGE_INSTRUCTIONS}}";
12662
13103
 
12663
- // src/core/filesystem/safe-merger.ts
12664
- var MANAGED_BEGIN_MARKER = "<!-- CODECONDUCTOR:BEGIN managed -->";
12665
- var MANAGED_END_MARKER = "<!-- CODECONDUCTOR:END managed -->";
12666
- function mergeManagedBlock(existing, incoming) {
12667
- validateMarkers(incoming, "incoming content");
12668
- if (!existing) {
12669
- return { content: incoming, action: "written" };
13104
+ // src/adapters/codex/codex-installer.ts
13105
+ class CodexInstaller {
13106
+ name = "codex";
13107
+ target = "codex";
13108
+ spec = null;
13109
+ setSpec(spec) {
13110
+ this.spec = spec;
12670
13111
  }
12671
- validateMarkers(existing, "existing content");
12672
- const existingBlock = getManagedBlockRange(existing);
12673
- const incomingBlock = getManagedBlockRange(incoming);
12674
- return {
12675
- content: existing.slice(0, existingBlock.start) + incoming.slice(incomingBlock.start, incomingBlock.end) + existing.slice(existingBlock.end),
12676
- action: "merged"
12677
- };
12678
- }
12679
- function validateMarkers(content, label) {
12680
- const beginCount = countOccurrences(content, MANAGED_BEGIN_MARKER);
12681
- const endCount = countOccurrences(content, MANAGED_END_MARKER);
12682
- if (beginCount !== 1 || endCount !== 1) {
12683
- throw new Error(`${label} must contain exactly one managed begin marker and one managed end marker`);
13112
+ async generate() {
13113
+ if (!this.spec) {
13114
+ throw new Error("Council spec not set");
13115
+ }
13116
+ return generateCodexFiles(this.spec);
12684
13117
  }
12685
- if (content.indexOf(MANAGED_BEGIN_MARKER) > content.indexOf(MANAGED_END_MARKER)) {
12686
- throw new Error(`${label} has managed markers in the wrong order`);
13118
+ async isAvailable() {
13119
+ return true;
12687
13120
  }
12688
13121
  }
12689
- function getManagedBlockRange(content) {
12690
- const start = content.indexOf(MANAGED_BEGIN_MARKER);
12691
- const end = content.indexOf(MANAGED_END_MARKER) + MANAGED_END_MARKER.length;
12692
- return { start, end };
12693
- }
12694
- function countOccurrences(content, needle) {
12695
- return content.split(needle).length - 1;
13122
+ function createCodexInstaller(spec) {
13123
+ const installer = new CodexInstaller;
13124
+ installer.setSpec(spec);
13125
+ return installer;
12696
13126
  }
12697
13127
 
12698
- // src/core/presets/file-copier.ts
12699
- async function listFilesRecursive(dir, base = dir) {
12700
- const entries = await readdir(dir, { withFileTypes: true });
13128
+ // src/adapters/opencode/opencode-council-generator.ts
13129
+ function generateOpenCodeFiles(spec) {
12701
13130
  const files = [];
12702
- for (const entry of entries) {
12703
- const full = join2(dir, entry.name);
12704
- if (entry.isDirectory()) {
12705
- files.push(...await listFilesRecursive(full, base));
12706
- } else {
12707
- files.push(relative(base, full));
12708
- }
13131
+ files.push({
13132
+ path: ".opencode/commands/cc-council.md",
13133
+ content: generateCouncilCommand(spec),
13134
+ overwrite: false
13135
+ });
13136
+ files.push({
13137
+ path: ".opencode/agents/council-lead.md",
13138
+ content: generateCouncilLead(spec),
13139
+ overwrite: false
13140
+ });
13141
+ for (const agent of spec.agents) {
13142
+ files.push({
13143
+ path: `.opencode/agents/council-${agent.id}.md`,
13144
+ content: generateOpenCodeAgentContent(agent),
13145
+ overwrite: false
13146
+ });
12709
13147
  }
12710
13148
  return files;
12711
13149
  }
12712
- async function resolveEntryFiles(entry, presetsDir, baseDir) {
12713
- const srcAbsolute = resolve5(presetsDir, entry.src);
12714
- try {
12715
- const s = await stat(srcAbsolute);
12716
- if (s.isDirectory()) {
12717
- const files = await listFilesRecursive(srcAbsolute);
12718
- return files.map((f) => ({
12719
- src: join2(srcAbsolute, f),
12720
- dest: resolve5(baseDir, entry.dest, f)
12721
- }));
12722
- }
12723
- return [{ src: srcAbsolute, dest: resolve5(baseDir, entry.dest) }];
12724
- } catch {
12725
- return [];
12726
- }
12727
- }
12728
- function mergeDeep(target, source) {
12729
- const result = { ...target };
12730
- for (const key of Object.keys(source)) {
12731
- const srcVal = source[key];
12732
- const tgtVal = target[key];
12733
- if (Array.isArray(srcVal) && Array.isArray(tgtVal)) {
12734
- result[key] = [...new Set([...tgtVal, ...srcVal])];
12735
- } else if (srcVal && typeof srcVal === "object" && !Array.isArray(srcVal) && tgtVal && typeof tgtVal === "object" && !Array.isArray(tgtVal)) {
12736
- result[key] = mergeDeep(tgtVal, srcVal);
12737
- } else {
12738
- result[key] = srcVal;
12739
- }
12740
- }
12741
- return result;
13150
+ function generateCouncilCommand(spec) {
13151
+ return `---
13152
+ description: ${yamlString(spec.description)}
13153
+ agent: council-lead
13154
+ subtask: true
13155
+ ---
13156
+
13157
+ Run the CodeConductor council for multi-perspective analysis.
13158
+
13159
+ ## Version
13160
+ ${spec.version}
13161
+
13162
+ ## Agents
13163
+ ${spec.agents.map((a) => `- ${a.role} (${a.id})`).join(`
13164
+ `)}
13165
+
13166
+ ## Instructions
13167
+ Coordinate with the council agents and synthesize their perspectives into the configured output contract.
13168
+ `;
12742
13169
  }
12743
- function extractAgentRole(filePath) {
12744
- const parts = filePath.replace(/\\/g, "/").split("/");
12745
- const basename2 = parts[parts.length - 1];
12746
- const name = basename2.replace(/\.md$/, "");
12747
- const knownRoles = [
12748
- "architect",
12749
- "implementer",
12750
- "tester",
12751
- "orchestrator",
12752
- "reviewer",
12753
- "docs",
12754
- "task-coach",
12755
- "repo-explorer"
12756
- ];
12757
- return knownRoles.includes(name) ? name : null;
13170
+ function generateCouncilLead(spec) {
13171
+ return `---
13172
+ description: ${yamlString(`${spec.description} Council lead. Coordinates council members and synthesizes recommendations.`)}
13173
+ mode: subagent
13174
+ permission:
13175
+ read: allow
13176
+ edit: deny
13177
+ bash: deny
13178
+ glob: allow
13179
+ grep: allow
13180
+ webfetch: deny
13181
+ websearch: deny
13182
+ ---
13183
+
13184
+ # Council Lead Agent
13185
+
13186
+ ## Role
13187
+ Coordinates the council and synthesizes perspectives
13188
+
13189
+ ## Version
13190
+ ${spec.version}
13191
+
13192
+ ## Council Members
13193
+ ${spec.agents.map((a) => `- ${a.role}: ${a.focus.join(", ")}`).join(`
13194
+ `)}
13195
+
13196
+ ## Responsibilities
13197
+ - Coordinate agent responses
13198
+ - Synthesize different perspectives
13199
+ - Identify consensus and disagreements
13200
+ - Provide final recommendation
13201
+ `;
12758
13202
  }
12759
- function renderTemplate(content, modelConfig, filePath, locale = "en") {
12760
- const cleanLocale = locale === "es" || locale === "en" ? locale : "en";
12761
- const templatedContent = content.replace(/\{\{COMMIT_STYLE\}\}/g, COMMIT_STYLE[cleanLocale]).replace(/\{\{COMMIT_WORKFLOW\}\}/g, COMMIT_WORKFLOW[cleanLocale]);
12762
- const agentRole = extractAgentRole(filePath);
12763
- if (agentRole && modelConfig.agents[agentRole]) {
12764
- const agentModels = modelConfig.agents[agentRole];
12765
- const targetModel = agentModels[modelConfig.target];
12766
- let result2 = templatedContent.replace(/\{\{MODEL\}\}/g, targetModel ?? "").replace(/\{\{MODEL_CLAUDE\}\}/g, agentModels.claude ?? "").replace(/\{\{MODEL_OPENCODE\}\}/g, agentModels.opencode ?? "").replace(/\{\{MODEL_CODEX\}\}/g, agentModels.codex ?? "").replace(/\{\{MODEL_GEMINI\}\}/g, agentModels.gemini ?? "").replace(/\{\{MODEL_CURSOR\}\}/g, agentModels.cursor ?? "").replace(new RegExp(LOCALE_PLACEHOLDER.replace(/[{}]/g, "\\$&"), "g"), getLanguageInstruction(locale));
12767
- if (modelConfig.tools || modelConfig.permissions) {
12768
- result2 = substituteToolNames(result2, modelConfig);
12769
- }
12770
- return result2;
12771
- }
12772
- const knownRoles = [
12773
- "orchestrator",
12774
- "task-coach",
12775
- "architect",
12776
- "implementer",
12777
- "tester",
12778
- "reviewer",
12779
- "docs",
12780
- "repo-explorer"
12781
- ];
12782
- let result = templatedContent;
12783
- for (const role of knownRoles) {
12784
- if (!modelConfig.agents[role])
12785
- continue;
12786
- const agentModels = modelConfig.agents[role];
12787
- const sectionRegex = new RegExp(`(### ${role}[\\s\\S]*?)(?=### |## |$)`, "gi");
12788
- const sectionMatch = result.match(sectionRegex);
12789
- if (sectionMatch) {
12790
- for (const section of sectionMatch) {
12791
- const renderedSection = section.replace(/\{\{MODEL_CLAUDE\}\}/g, agentModels.claude ?? "").replace(/\{\{MODEL_OPENCODE\}\}/g, agentModels.opencode ?? "").replace(/\{\{MODEL_CODEX\}\}/g, agentModels.codex ?? "").replace(/\{\{MODEL_GEMINI\}\}/g, agentModels.gemini ?? "").replace(/\{\{MODEL_CURSOR\}\}/g, agentModels.cursor ?? "");
12792
- result = result.replace(section, renderedSection);
12793
- }
12794
- }
12795
- }
12796
- result = result.replace(new RegExp(LOCALE_PLACEHOLDER.replace(/[{}]/g, "\\$&"), "g"), getLanguageInstruction(locale));
12797
- if (modelConfig.tools || modelConfig.permissions) {
12798
- result = substituteToolNames(result, modelConfig);
12799
- }
12800
- return result;
13203
+ function generateOpenCodeAgentContent(agent) {
13204
+ return `---
13205
+ description: ${yamlString(`${agent.role} council agent. Focus: ${agent.focus.join(", ")}. Context: ${agent.context}. Model hint: ${agent.modelHint}.`)}
13206
+ mode: subagent
13207
+ permission:
13208
+ ${generatePermissionBlock(agent.context)}
13209
+ ---
13210
+
13211
+ ${generateAgentContent(agent)}`;
12801
13212
  }
12802
- function substituteToolNames(content, modelConfig) {
12803
- if (!modelConfig.tools && !modelConfig.permissions)
12804
- return content;
12805
- const target = modelConfig.target;
12806
- const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
12807
- if (!fmMatch)
12808
- return content;
12809
- const frontmatter = fmMatch[1];
12810
- if (target === "opencode" && modelConfig.permissions) {
12811
- const updatedFrontmatter2 = frontmatter.replace(/^tools:\s*(.+)\r?\n?/m, "");
12812
- return content.replace(fmMatch[0], `---
12813
- ${updatedFrontmatter2}
12814
- ---`);
13213
+ function generatePermissionBlock(context) {
13214
+ if (context === "repo-readonly") {
13215
+ return ` read: allow
13216
+ edit: deny
13217
+ bash: deny
13218
+ glob: allow
13219
+ grep: allow
13220
+ webfetch: deny
13221
+ websearch: deny`;
12815
13222
  }
12816
- if (!modelConfig.tools)
12817
- return content;
12818
- const updatedFrontmatter = frontmatter.replace(/^tools:\s*(.+)$/m, (_match, toolsLine) => {
12819
- const baseNames = toolsLine.split(",").map((t) => t.trim());
12820
- const mappedNames = baseNames.map((baseName) => {
12821
- const toolMapping = modelConfig.tools?.[baseName];
12822
- if (toolMapping && toolMapping[target]) {
12823
- return toolMapping[target];
12824
- }
12825
- return baseName;
12826
- });
12827
- return `tools: ${mappedNames.join(", ")}`;
12828
- });
12829
- return content.replace(fmMatch[0], `---
12830
- ${updatedFrontmatter}
12831
- ---`);
13223
+ return ` read: deny
13224
+ edit: deny
13225
+ bash: deny
13226
+ glob: deny
13227
+ grep: deny
13228
+ webfetch: deny
13229
+ websearch: deny`;
13230
+ }
13231
+ function yamlString(value) {
13232
+ return JSON.stringify(value);
12832
13233
  }
12833
- async function applySingleFile(srcPath, destPath, strategy, force, dryRun, isTemplate, modelConfig, locale) {
12834
- if (strategy === "skip") {
12835
- return { src: srcPath, dest: destPath, action: "skipped", dryRun };
12836
- }
12837
- let content;
12838
- try {
12839
- content = await readFile4(srcPath, "utf-8");
12840
- } catch (e) {
12841
- return { src: srcPath, dest: destPath, action: "error", error: `Cannot read source: ${e}` };
12842
- }
12843
- const incomingContent = isTemplate && modelConfig ? renderTemplate(content, modelConfig, srcPath, locale) : content;
12844
- let finalContent = incomingContent;
12845
- let action = "written";
12846
- if (strategy === "append") {
12847
- let existing = "";
12848
- try {
12849
- existing = await readFile4(destPath, "utf-8");
12850
- } catch {}
12851
- if (existing) {
12852
- finalContent = existing + `
12853
-
12854
- ---
12855
13234
 
12856
- ` + incomingContent;
12857
- }
12858
- action = "appended";
12859
- } else if (strategy === "merge-json") {
12860
- let existing = {};
12861
- try {
12862
- existing = JSON.parse(await readFile4(destPath, "utf-8"));
12863
- } catch {}
12864
- try {
12865
- const incoming = JSON.parse(incomingContent);
12866
- finalContent = JSON.stringify(mergeDeep(existing, incoming), null, 2);
12867
- } catch (e) {
12868
- return { src: srcPath, dest: destPath, action: "error", error: `JSON merge failed: ${e}` };
12869
- }
12870
- action = "merged";
12871
- } else if (strategy === "merge-managed") {
12872
- let existing = null;
12873
- try {
12874
- existing = await readFile4(destPath, "utf-8");
12875
- } catch {}
12876
- try {
12877
- const merged = mergeManagedBlock(existing, incomingContent);
12878
- finalContent = merged.content;
12879
- action = merged.action;
12880
- } catch (e) {
12881
- return { src: srcPath, dest: destPath, action: "error", error: `Managed merge failed: ${e}` };
12882
- }
13235
+ // src/adapters/opencode/opencode-installer.ts
13236
+ class OpenCodeInstaller {
13237
+ name = "opencode";
13238
+ target = "opencode";
13239
+ spec = null;
13240
+ setSpec(spec) {
13241
+ this.spec = spec;
12883
13242
  }
12884
- if (dryRun) {
12885
- return { src: srcPath, dest: destPath, action, dryRun: true };
13243
+ async generate() {
13244
+ if (!this.spec) {
13245
+ throw new Error("Council spec not set");
13246
+ }
13247
+ return generateOpenCodeFiles(this.spec);
12886
13248
  }
12887
- try {
12888
- await mkdir4(dirname3(destPath), { recursive: true });
12889
- await writeFile4(destPath, finalContent, "utf-8");
12890
- return { src: srcPath, dest: destPath, action };
12891
- } catch (e) {
12892
- return { src: srcPath, dest: destPath, action: "error", error: String(e) };
13249
+ async isAvailable() {
13250
+ return true;
12893
13251
  }
12894
13252
  }
12895
- async function copyFromManifest(manifest, presetsDir, baseDir, isGlobal, dryRun, force, modelConfig = null, locale = "en") {
13253
+ function createOpenCodeInstaller(spec) {
13254
+ const installer = new OpenCodeInstaller;
13255
+ installer.setSpec(spec);
13256
+ return installer;
13257
+ }
13258
+
13259
+ // src/core/filesystem/file-writer.ts
13260
+ import { access as access4, mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
13261
+ import { dirname as dirname3 } from "node:path";
13262
+ init_safety();
13263
+ async function writeGeneratedFiles(files, options) {
12896
13264
  const results = [];
12897
- for (const entry of manifest.entries) {
12898
- const strategy = isGlobal && entry.globalStrategy ? entry.globalStrategy : entry.strategy;
12899
- let resolvedBaseDir = baseDir;
12900
- let resolvedEntry = entry;
12901
- if (manifest.target === "agy" && isGlobal) {
12902
- resolvedBaseDir = join2(homedir2(), ".gemini", "config");
12903
- resolvedEntry = {
12904
- ...entry,
12905
- dest: entry.dest.replace(/^\.agents\/?/, "")
12906
- };
13265
+ for (const file of files) {
13266
+ if (!validateWritePath(file.path)) {
13267
+ results.push({
13268
+ path: file.path,
13269
+ success: false,
13270
+ error: "Protected path"
13271
+ });
13272
+ continue;
12907
13273
  }
12908
- const files = await resolveEntryFiles(resolvedEntry, presetsDir, resolvedBaseDir);
12909
- const isTemplate = entry.template === true;
12910
- for (const { src, dest } of files) {
12911
- results.push(await applySingleFile(src, dest, strategy, force, dryRun, isTemplate, modelConfig, locale));
13274
+ if (options.dryRun) {
13275
+ results.push({
13276
+ path: file.path,
13277
+ success: true
13278
+ });
13279
+ continue;
13280
+ }
13281
+ if (!options.force) {
13282
+ try {
13283
+ await access4(file.path);
13284
+ results.push({
13285
+ path: file.path,
13286
+ success: false,
13287
+ error: "File exists, use --force to overwrite"
13288
+ });
13289
+ continue;
13290
+ } catch {}
13291
+ }
13292
+ try {
13293
+ const dir = dirname3(file.path);
13294
+ await mkdir4(dir, { recursive: true });
13295
+ await writeFile4(file.path, file.content, "utf-8");
13296
+ results.push({
13297
+ path: file.path,
13298
+ success: true
13299
+ });
13300
+ } catch (error) {
13301
+ results.push({
13302
+ path: file.path,
13303
+ success: false,
13304
+ error: String(error)
13305
+ });
12912
13306
  }
12913
13307
  }
12914
13308
  return results;
12915
13309
  }
12916
13310
 
12917
- // src/core/presets/manifest-loader.ts
12918
- import { readFile as readFile5 } from "node:fs/promises";
12919
- import { join as join3 } from "node:path";
12920
- var MANIFESTS_DIR = join3(SRC_PRESETS_DIR, "manifests");
12921
- var MODELS_DIR = join3(SRC_PRESETS_DIR, "models");
12922
- var PRESETS_DIR = ROOT_PRESETS_DIR;
12923
- async function loadManifest(target) {
12924
- const manifestPath = join3(MANIFESTS_DIR, `${target}.yml`);
12925
- const content = await readFile5(manifestPath, "utf-8");
12926
- const data = $parse(content);
12927
- return InstallManifestSchema.parse(data);
12928
- }
12929
- async function loadModelConfig(target) {
12930
- const modelPath = join3(MODELS_DIR, `${target}.yml`);
12931
- const content = await readFile5(modelPath, "utf-8");
12932
- const data = $parse(content);
12933
- return ModelConfigSchema.parse(data);
12934
- }
12935
-
12936
13311
  // src/core/presets/preset-loader.ts
12937
- import { readFile as readFile6 } from "node:fs/promises";
12938
- import { resolve as resolve6 } from "node:path";
13312
+ import { readFile as readFile7 } from "node:fs/promises";
13313
+ import { resolve as resolve7 } from "node:path";
12939
13314
  async function loadPreset(name, projectRoot = process.cwd()) {
12940
13315
  const candidates = [
12941
- resolve6(projectRoot, ".codeconductor", "presets", `${name}.yml`),
12942
- resolve6(SRC_PRESETS_DIR, name, `${name}.yml`)
13316
+ resolve7(projectRoot, ".codeconductor", "presets", `${name}.yml`),
13317
+ resolve7(SRC_PRESETS_DIR, name, `${name}.yml`)
12943
13318
  ];
12944
13319
  for (const presetPath of candidates) {
12945
13320
  try {
12946
- const content = await readFile6(presetPath, "utf-8");
13321
+ const content = await readFile7(presetPath, "utf-8");
12947
13322
  const data = $parse(content);
12948
13323
  if (!data)
12949
13324
  continue;
@@ -12987,7 +13362,7 @@ function getIndividualTargets(target) {
12987
13362
  // src/commands/install.command.ts
12988
13363
  async function installCommand(options) {
12989
13364
  const { target, dryRun, force, global: isGlobal, output, projectRoot } = options;
12990
- const baseDir = isGlobal ? homedir3() : projectRoot;
13365
+ const baseDir = isGlobal ? homedir5() : projectRoot;
12991
13366
  try {
12992
13367
  const runnerTarget = parseRunnerTarget(target);
12993
13368
  const targets = getIndividualTargets(runnerTarget);
@@ -13023,7 +13398,7 @@ async function installCommand(options) {
13023
13398
  const generatedFiles = await installer.generate();
13024
13399
  const resolvedFiles = generatedFiles.map((f) => ({
13025
13400
  ...f,
13026
- path: resolve7(baseDir, f.path)
13401
+ path: resolve8(baseDir, f.path)
13027
13402
  }));
13028
13403
  const results = await writeGeneratedFiles(resolvedFiles, writeOptions);
13029
13404
  for (const result of results) {
@@ -13082,7 +13457,7 @@ async function installCommand(options) {
13082
13457
  }
13083
13458
  async function installPresetCommand(options) {
13084
13459
  const { target, dryRun, force, global: isGlobal, projectRoot } = options;
13085
- const baseDir = isGlobal ? homedir3() : projectRoot;
13460
+ const baseDir = isGlobal ? homedir5() : projectRoot;
13086
13461
  try {
13087
13462
  const runnerTarget = parseRunnerTarget(target);
13088
13463
  const targets = getIndividualTargets(runnerTarget);
@@ -13132,8 +13507,8 @@ async function installPresetCommand(options) {
13132
13507
  }
13133
13508
 
13134
13509
  // src/commands/install-lsp.command.ts
13135
- import { homedir as homedir5 } from "node:os";
13136
- import { resolve as resolve8 } from "node:path";
13510
+ import { homedir as homedir7 } from "node:os";
13511
+ import { resolve as resolve9 } from "node:path";
13137
13512
 
13138
13513
  // src/core/lsp/lsp-config-utils.ts
13139
13514
  function getLanguageServerConfig(lspIds) {
@@ -13377,15 +13752,15 @@ function createOpenCodeLspGenerator() {
13377
13752
  // src/core/lsp/lsp-installer.ts
13378
13753
  import { execFile } from "node:child_process";
13379
13754
  import { access as access5, mkdir as mkdir5 } from "node:fs/promises";
13380
- import { homedir as homedir4 } from "node:os";
13381
- import { join as join4 } from "node:path";
13755
+ import { homedir as homedir6 } from "node:os";
13756
+ import { join as join5 } from "node:path";
13382
13757
  import { promisify } from "node:util";
13383
13758
  var execFileAsync = promisify(execFile);
13384
13759
 
13385
13760
  class LspInstaller {
13386
13761
  lspBinDir;
13387
13762
  constructor() {
13388
- this.lspBinDir = join4(homedir4(), ".codeconductor", "lsp", "bin");
13763
+ this.lspBinDir = join5(homedir6(), ".codeconductor", "lsp", "bin");
13389
13764
  }
13390
13765
  async checkInstalled(def) {
13391
13766
  try {
@@ -13512,7 +13887,7 @@ class LspInstaller {
13512
13887
  throw new Error(`No binary available for platform: ${platformKey}`);
13513
13888
  }
13514
13889
  await mkdir5(this.lspBinDir, { recursive: true });
13515
- const destPath = join4(this.lspBinDir, def.binaryName);
13890
+ const destPath = join5(this.lspBinDir, def.binaryName);
13516
13891
  try {
13517
13892
  await access5(destPath);
13518
13893
  return;
@@ -13619,7 +13994,7 @@ function resolveLsps(languages) {
13619
13994
  // src/commands/install-lsp.command.ts
13620
13995
  async function installLspCommand(options) {
13621
13996
  const { target, lang, dryRun, force, global: isGlobal, output, projectRoot } = options;
13622
- const baseDir = isGlobal ? homedir5() : projectRoot;
13997
+ const baseDir = isGlobal ? homedir7() : projectRoot;
13623
13998
  try {
13624
13999
  const runnerTarget = parseRunnerTarget(target);
13625
14000
  const targets = getIndividualTargets(runnerTarget);
@@ -13665,7 +14040,7 @@ async function installLspCommand(options) {
13665
14040
  const generatedFiles = generator.generate(installReport.results);
13666
14041
  const resolvedFiles = generatedFiles.map((f) => ({
13667
14042
  ...f,
13668
- path: resolve8(baseDir, f.path)
14043
+ path: resolve9(baseDir, f.path)
13669
14044
  }));
13670
14045
  const results = await writeGeneratedFiles(resolvedFiles, writeOptions);
13671
14046
  for (const result of results) {
@@ -13759,7 +14134,7 @@ function getLspConfigGenerator(target) {
13759
14134
 
13760
14135
  // src/commands/seo-audit.command.ts
13761
14136
  import { writeFile as writeFile5, mkdir as mkdir6 } from "node:fs/promises";
13762
- import { dirname as dirname4, resolve as resolve9 } from "node:path";
14137
+ import { dirname as dirname4, resolve as resolve10 } from "node:path";
13763
14138
 
13764
14139
  // src/infrastructure/http/safe-fetch.ts
13765
14140
  import { lookup } from "node:dns/promises";
@@ -13846,7 +14221,7 @@ async function safeFetch(urlString, options = {}) {
13846
14221
  }
13847
14222
  }
13848
14223
  async function delay(ms) {
13849
- return new Promise((resolve9) => setTimeout(resolve9, ms));
14224
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
13850
14225
  }
13851
14226
 
13852
14227
  // src/infrastructure/parsers/sitemap-parser.ts
@@ -15050,12 +15425,12 @@ async function seoAuditCommand(options) {
15050
15425
  formattedOutput = formatCli(report);
15051
15426
  }
15052
15427
  if (output) {
15053
- const outputPath = resolve9(options.projectRoot, output);
15428
+ const outputPath = resolve10(options.projectRoot, output);
15054
15429
  await mkdir6(dirname4(outputPath), { recursive: true });
15055
15430
  await writeFile5(outputPath, formattedOutput, "utf-8");
15056
15431
  } else if (format === "markdown") {
15057
15432
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
15058
- const defaultPath = resolve9(options.projectRoot, "seo-reports", `audit-report-${timestamp}.md`);
15433
+ const defaultPath = resolve10(options.projectRoot, "seo-reports", `audit-report-${timestamp}.md`);
15059
15434
  await mkdir6(dirname4(defaultPath), { recursive: true });
15060
15435
  await writeFile5(defaultPath, formattedOutput, "utf-8");
15061
15436
  process.stderr.write(`Report saved to: ${defaultPath}
@@ -15069,7 +15444,7 @@ async function seoAuditCommand(options) {
15069
15444
  command: "seo audit",
15070
15445
  report,
15071
15446
  output: formattedOutput,
15072
- outputFile: output ? resolve9(options.projectRoot, output) : format === "markdown" ? resolve9(options.projectRoot, "seo-reports") : undefined
15447
+ outputFile: output ? resolve10(options.projectRoot, output) : format === "markdown" ? resolve10(options.projectRoot, "seo-reports") : undefined
15073
15448
  }
15074
15449
  };
15075
15450
  } catch (error) {
@@ -15086,7 +15461,7 @@ async function seoAuditCommand(options) {
15086
15461
 
15087
15462
  // src/commands/seo-llms.command.ts
15088
15463
  import { writeFile as writeFile6, mkdir as mkdir7 } from "node:fs/promises";
15089
- import { dirname as dirname5, resolve as resolve10 } from "node:path";
15464
+ import { dirname as dirname5, resolve as resolve11 } from "node:path";
15090
15465
 
15091
15466
  // src/domain/seo/llms-generator.ts
15092
15467
  function extractTitle2(html) {
@@ -15229,7 +15604,7 @@ async function seoLlmsCommand(options) {
15229
15604
  }
15230
15605
  }) : await generateLlmsTxtFromUrl(url);
15231
15606
  process.stderr.write("\r" + " ".repeat(80) + "\r");
15232
- const outputPath = output ? resolve10(options.projectRoot, output) : resolve10(options.projectRoot, "llms.txt");
15607
+ const outputPath = output ? resolve11(options.projectRoot, output) : resolve11(options.projectRoot, "llms.txt");
15233
15608
  await mkdir7(dirname5(outputPath), { recursive: true });
15234
15609
  await writeFile6(outputPath, result.content, "utf-8");
15235
15610
  process.stderr.write(`Generated: ${outputPath} (${result.entries.length} entries)
@@ -15257,10 +15632,14 @@ async function seoLlmsCommand(options) {
15257
15632
  }
15258
15633
 
15259
15634
  // src/commands/update.command.ts
15635
+ import { homedir as homedir8 } from "node:os";
15636
+ import { resolve as resolve12, dirname as dirname6 } from "node:path";
15637
+ import { mkdir as mkdir8, readFile as readFile8, writeFile as writeFile7, stat as stat3 } from "node:fs/promises";
15260
15638
  async function updateCommand(options) {
15261
- const { dryRun, force, output, projectRoot } = options;
15639
+ const { dryRun, force, global: isGlobal, output, projectRoot } = options;
15640
+ const basePath = isGlobal ? homedir8() : projectRoot;
15262
15641
  try {
15263
- const configResult = await loadConfig(projectRoot);
15642
+ const configResult = await loadConfig(basePath);
15264
15643
  if (!configResult.success) {
15265
15644
  return {
15266
15645
  code: 1,
@@ -15272,42 +15651,40 @@ async function updateCommand(options) {
15272
15651
  };
15273
15652
  }
15274
15653
  const config = configResult.data;
15275
- if (!config.presets.council.enabled) {
15276
- return {
15277
- code: 4,
15278
- data: {
15279
- success: false,
15280
- command: "update",
15281
- errors: ["Council preset is not enabled"]
15282
- }
15283
- };
15284
- }
15285
- const presetResult = await loadCouncilPreset(projectRoot);
15286
- if (!presetResult.success) {
15287
- return {
15288
- code: 1,
15289
- data: {
15290
- success: false,
15291
- command: "update",
15292
- errors: ["Failed to load preset"]
15293
- }
15294
- };
15654
+ const updateResults = await checkUpdates(basePath, isGlobal);
15655
+ const largeFiles = await validateAgentFileSizes(basePath, isGlobal);
15656
+ if (largeFiles.length > 0) {
15657
+ largeFiles.forEach((file) => {
15658
+ console.warn(`[WARNING] File exceeds 40KB: ${file.path} (${(file.size / 1024).toFixed(1)} KB)`);
15659
+ });
15295
15660
  }
15296
- const spec = presetResult.data;
15297
- const currentVersion = config.presets.council.version;
15298
- const newVersion = spec.version;
15299
- if (currentVersion === newVersion) {
15661
+ if (!updateResults.hasUpdates) {
15300
15662
  return {
15301
15663
  code: 0,
15302
15664
  data: {
15303
15665
  success: true,
15304
15666
  command: "update",
15305
15667
  message: "Already up to date",
15306
- currentVersion,
15307
- newVersion
15668
+ wouldUpdate: [],
15669
+ updated: []
15308
15670
  }
15309
15671
  };
15310
15672
  }
15673
+ const wouldUpdate = [];
15674
+ if (updateResults.council)
15675
+ wouldUpdate.push("council preset");
15676
+ if (updateResults.policy)
15677
+ wouldUpdate.push("policy file");
15678
+ for (const t of updateResults.targets) {
15679
+ if (t.hasUpdate) {
15680
+ wouldUpdate.push(`${t.target} target files`);
15681
+ }
15682
+ }
15683
+ for (const s of updateResults.skills) {
15684
+ if (s.hasUpdate) {
15685
+ wouldUpdate.push(`skill ${s.id} to v${s.latestVersion}`);
15686
+ }
15687
+ }
15311
15688
  if (dryRun) {
15312
15689
  return {
15313
15690
  code: 0,
@@ -15315,48 +15692,77 @@ async function updateCommand(options) {
15315
15692
  success: true,
15316
15693
  command: "update",
15317
15694
  message: "Dry run - would update",
15318
- currentVersion,
15319
- newVersion,
15320
- wouldUpdate: ["council preset files"]
15695
+ wouldUpdate,
15696
+ updated: []
15321
15697
  }
15322
15698
  };
15323
15699
  }
15324
- const writeOptions = { dryRun: false, force };
15325
- const target = config.defaults.target;
15326
- let installer;
15327
- switch (target) {
15328
- case "opencode":
15329
- installer = createOpenCodeInstaller(spec);
15330
- break;
15331
- case "claude":
15332
- installer = createClaudeInstaller(spec);
15333
- break;
15334
- case "codex":
15335
- installer = createCodexInstaller(spec);
15336
- break;
15337
- default:
15338
- return {
15339
- code: 1,
15340
- data: {
15341
- success: false,
15342
- command: "update",
15343
- errors: [`Unknown target: ${target}`]
15700
+ const updated = [];
15701
+ if (updateResults.council) {
15702
+ const localCouncil = resolve12(basePath, ".codeconductor", "presets", "council.yml");
15703
+ const bundledCouncil = resolve12(SRC_PRESETS_DIR, "council", "council.yml");
15704
+ try {
15705
+ const content = await readFile8(bundledCouncil, "utf-8");
15706
+ await mkdir8(dirname6(localCouncil), { recursive: true });
15707
+ await writeFile7(localCouncil, content, "utf-8");
15708
+ updated.push(localCouncil);
15709
+ } catch (e) {
15710
+ throw new Error(`Failed to update council.yml: ${e}`);
15711
+ }
15712
+ }
15713
+ if (updateResults.policy) {
15714
+ const localPolicy = resolve12(basePath, ".codeconductor", "presets", "policy.yml");
15715
+ try {
15716
+ const content = await readFile8(POLICY_PATH, "utf-8");
15717
+ await mkdir8(dirname6(localPolicy), { recursive: true });
15718
+ await writeFile7(localPolicy, content, "utf-8");
15719
+ updated.push(localPolicy);
15720
+ } catch (e) {
15721
+ throw new Error(`Failed to update policy.yml: ${e}`);
15722
+ }
15723
+ }
15724
+ const locale = config?.defaults?.locale ?? "en";
15725
+ for (const t of updateResults.targets) {
15726
+ if (t.hasUpdate) {
15727
+ const targetName = t.target;
15728
+ const manifest = await loadManifest(targetName);
15729
+ const modelConfig = await loadModelConfig(targetName);
15730
+ const results = await copyFromManifest(manifest, PRESETS_DIR, basePath, isGlobal, false, force, modelConfig, locale);
15731
+ for (const r of results) {
15732
+ if (r.action !== "skipped" && r.action !== "error") {
15733
+ updated.push(r.dest);
15734
+ } else if (r.action === "error") {
15735
+ throw new Error(`Failed to copy target file: ${r.error}`);
15344
15736
  }
15345
- };
15737
+ }
15738
+ }
15346
15739
  }
15347
- const files = await installer.generate();
15348
- const results = await writeGeneratedFiles(files, writeOptions);
15349
- const updated = results.filter((r) => r.success).map((r) => r.path);
15350
- const errors3 = results.filter((r) => !r.success).map((r) => `${r.path}: ${r.error}`);
15351
- if (errors3.length > 0) {
15352
- return {
15353
- code: 2,
15354
- data: {
15355
- success: false,
15356
- command: "update",
15357
- errors: errors3
15740
+ if (updateResults.skills.some((s) => s.hasUpdate)) {
15741
+ const newSkillsLock = {};
15742
+ const currentLock = await loadSkillsLock(basePath) || {};
15743
+ for (const [id, ver] of Object.entries(currentLock)) {
15744
+ newSkillsLock[id] = ver;
15745
+ }
15746
+ for (const s of updateResults.skills) {
15747
+ newSkillsLock[s.id] = s.latestVersion;
15748
+ }
15749
+ let lockDest = resolve12(basePath, ".codeconductor", "skills-lock.json");
15750
+ try {
15751
+ const statAgents = await stat3(resolve12(basePath, ".agents"));
15752
+ if (statAgents.isDirectory()) {
15753
+ const statCodeConductor = await stat3(resolve12(basePath, ".codeconductor")).catch(() => null);
15754
+ if (!statCodeConductor) {
15755
+ lockDest = resolve12(basePath, ".agents", "skills-lock.json");
15756
+ }
15358
15757
  }
15359
- };
15758
+ } catch {}
15759
+ try {
15760
+ await mkdir8(dirname6(lockDest), { recursive: true });
15761
+ await writeFile7(lockDest, JSON.stringify(newSkillsLock, null, 2), "utf-8");
15762
+ updated.push(lockDest);
15763
+ } catch (e) {
15764
+ throw new Error(`Failed to write skills-lock.json: ${e}`);
15765
+ }
15360
15766
  }
15361
15767
  return {
15362
15768
  code: 0,
@@ -15364,8 +15770,7 @@ async function updateCommand(options) {
15364
15770
  success: true,
15365
15771
  command: "update",
15366
15772
  message: "Updated successfully",
15367
- currentVersion,
15368
- newVersion,
15773
+ wouldUpdate: [],
15369
15774
  updated
15370
15775
  }
15371
15776
  };
@@ -15570,6 +15975,7 @@ async function routeCommand(args, projectRoot) {
15570
15975
  projectRoot,
15571
15976
  dryRun: flags.dryRun,
15572
15977
  force: flags.force,
15978
+ global: options.global === true || options.global === "true",
15573
15979
  output: flags.output
15574
15980
  });
15575
15981
  case "seo": {