helixevo 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2075,28 +2075,93 @@ function ensureDir(dir) {
2075
2075
  mkdirSync(dir, { recursive: true });
2076
2076
  }
2077
2077
  }
2078
+ function providerConfigKey(provider) {
2079
+ if (provider === "claude-code")
2080
+ return "claudeCode";
2081
+ if (provider === "codex")
2082
+ return "codex";
2083
+ return "ollama";
2084
+ }
2085
+ function uniqueProviders(providers) {
2086
+ return [...new Set(providers.filter((provider) => provider === "claude-code" || provider === "codex" || provider === "ollama"))];
2087
+ }
2088
+ function syncTopLevelModels(config) {
2089
+ const providerKey = providerConfigKey(config.llm.defaultProvider);
2090
+ const providerConfig = config.llm.providers[providerKey];
2091
+ return {
2092
+ ...config,
2093
+ model: providerConfig.model,
2094
+ judgeModel: providerConfig.judgeModel
2095
+ };
2096
+ }
2097
+ function normalizeConfig(raw) {
2098
+ const input = raw ?? {};
2099
+ const inputProviders = input.llm?.providers;
2100
+ const claudeCode = {
2101
+ ...DEFAULT_CONFIG.llm.providers.claudeCode,
2102
+ ...inputProviders?.claudeCode ?? {},
2103
+ model: inputProviders?.claudeCode?.model ?? input.model ?? DEFAULT_CONFIG.llm.providers.claudeCode.model,
2104
+ judgeModel: inputProviders?.claudeCode?.judgeModel ?? input.judgeModel ?? DEFAULT_CONFIG.llm.providers.claudeCode.judgeModel
2105
+ };
2106
+ const codex = {
2107
+ ...DEFAULT_CONFIG.llm.providers.codex,
2108
+ ...inputProviders?.codex ?? {}
2109
+ };
2110
+ const ollama = {
2111
+ ...DEFAULT_CONFIG.llm.providers.ollama,
2112
+ ...inputProviders?.ollama ?? {}
2113
+ };
2114
+ const fallbackOrder = uniqueProviders(input.llm?.fallbackOrder ?? DEFAULT_CONFIG.llm.fallbackOrder);
2115
+ const normalized = {
2116
+ model: input.model ?? DEFAULT_CONFIG.model,
2117
+ judgeModel: input.judgeModel ?? DEFAULT_CONFIG.judgeModel,
2118
+ llm: {
2119
+ defaultProvider: input.llm?.defaultProvider ?? DEFAULT_CONFIG.llm.defaultProvider,
2120
+ fallbackPolicy: input.llm?.fallbackPolicy ?? DEFAULT_CONFIG.llm.fallbackPolicy,
2121
+ fallbackOrder,
2122
+ providers: {
2123
+ claudeCode,
2124
+ codex,
2125
+ ollama
2126
+ }
2127
+ },
2128
+ evolution: {
2129
+ ...DEFAULT_CONFIG.evolution,
2130
+ ...input.evolution ?? {}
2131
+ },
2132
+ quality: {
2133
+ ...DEFAULT_CONFIG.quality,
2134
+ ...input.quality ?? {}
2135
+ },
2136
+ reporting: {
2137
+ ...DEFAULT_CONFIG.reporting,
2138
+ ...input.reporting ?? {}
2139
+ },
2140
+ paths: {
2141
+ ...DEFAULT_CONFIG.paths,
2142
+ ...input.paths ?? {}
2143
+ }
2144
+ };
2145
+ if (normalized.paths.data.includes(".skillgraph")) {
2146
+ normalized.paths.data = HELIX_DIR;
2147
+ }
2148
+ if (normalized.paths.generalSkills.includes(".skillgraph")) {
2149
+ normalized.paths.generalSkills = join(HELIX_DIR, "general");
2150
+ }
2151
+ normalized.llm.fallbackOrder = uniqueProviders(normalized.llm.fallbackOrder.filter((provider) => provider !== normalized.llm.defaultProvider));
2152
+ return syncTopLevelModels(normalized);
2153
+ }
2078
2154
  function loadConfig() {
2079
2155
  if (_configCache)
2080
2156
  return _configCache;
2081
- let config;
2082
- if (!existsSync(CONFIG_PATH)) {
2083
- config = DEFAULT_CONFIG;
2084
- } else {
2085
- const raw = readFileSync(CONFIG_PATH, "utf-8");
2086
- config = { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
2087
- }
2088
- if (config.paths.data.includes(".skillgraph")) {
2089
- config.paths.data = HELIX_DIR;
2090
- }
2091
- if (config.paths.generalSkills.includes(".skillgraph")) {
2092
- config.paths.generalSkills = join(HELIX_DIR, "general");
2093
- }
2157
+ const config = existsSync(CONFIG_PATH) ? normalizeConfig(JSON.parse(readFileSync(CONFIG_PATH, "utf-8"))) : normalizeConfig(undefined);
2094
2158
  _configCache = config;
2095
2159
  return config;
2096
2160
  }
2097
2161
  function saveConfig(config) {
2098
2162
  ensureDir(HELIX_DIR);
2099
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
2163
+ writeFileSync(CONFIG_PATH, JSON.stringify(normalizeConfig(config), null, 2));
2164
+ _configCache = null;
2100
2165
  }
2101
2166
  function getDataPath(filename) {
2102
2167
  const config = loadConfig();
@@ -2113,6 +2178,32 @@ var init_config = __esm(() => {
2113
2178
  DEFAULT_CONFIG = {
2114
2179
  model: "sonnet",
2115
2180
  judgeModel: "sonnet",
2181
+ llm: {
2182
+ defaultProvider: "claude-code",
2183
+ fallbackPolicy: "disabled",
2184
+ fallbackOrder: [],
2185
+ providers: {
2186
+ claudeCode: {
2187
+ enabled: true,
2188
+ command: "claude",
2189
+ model: "sonnet",
2190
+ judgeModel: "sonnet"
2191
+ },
2192
+ codex: {
2193
+ enabled: false,
2194
+ command: "codex",
2195
+ model: "gpt-5-codex",
2196
+ judgeModel: "gpt-5-codex"
2197
+ },
2198
+ ollama: {
2199
+ enabled: false,
2200
+ command: "ollama",
2201
+ host: "http://127.0.0.1:11434",
2202
+ model: "qwen3-coder:latest",
2203
+ judgeModel: "qwen3-coder:latest"
2204
+ }
2205
+ }
2206
+ },
2116
2207
  evolution: {
2117
2208
  schedule: "0 2 * * *",
2118
2209
  generalizationSchedule: "0 3 * * 5",
@@ -9647,6 +9738,68 @@ function loadGovernanceState() {
9647
9738
  return state;
9648
9739
  return { ...DEFAULT_GOVERNANCE_STATE, ...state, selectionMode: state.selectionMode === "manual" && state.manualMode ? "manual" : "auto" };
9649
9740
  }
9741
+ function providerSnapshotDefaults(provider) {
9742
+ const config = loadConfig();
9743
+ const providerConfig = provider === "claude-code" ? config.llm.providers.claudeCode : provider === "codex" ? config.llm.providers.codex : config.llm.providers.ollama;
9744
+ return {
9745
+ provider,
9746
+ configured: providerConfig.enabled,
9747
+ enabled: providerConfig.enabled,
9748
+ installed: false,
9749
+ available: false,
9750
+ status: "unknown",
9751
+ model: providerConfig.model,
9752
+ judgeModel: providerConfig.judgeModel,
9753
+ summary: providerConfig.enabled ? `No ${provider} execution has been recorded yet.` : `${provider} is installed as an optional provider but not enabled in config.`,
9754
+ nextStep: providerConfig.enabled ? `Run a provider-backed command or refresh provider status to record live ${provider} health.` : `Enable ${provider} in ~/.helix/config.json before selecting it.`,
9755
+ notices: []
9756
+ };
9757
+ }
9758
+ function defaultLlmRuntimeState() {
9759
+ const config = loadConfig();
9760
+ return {
9761
+ updatedAt: new Date(0).toISOString(),
9762
+ defaultProvider: config.llm.defaultProvider,
9763
+ fallbackPolicy: config.llm.fallbackPolicy,
9764
+ fallbackOrder: [...config.llm.fallbackOrder],
9765
+ providers: {
9766
+ "claude-code": providerSnapshotDefaults("claude-code"),
9767
+ codex: providerSnapshotDefaults("codex"),
9768
+ ollama: providerSnapshotDefaults("ollama")
9769
+ }
9770
+ };
9771
+ }
9772
+ function loadLlmRuntimeState() {
9773
+ const defaults = defaultLlmRuntimeState();
9774
+ const stored = readJson("llm-runtime-state.json", defaults);
9775
+ return {
9776
+ ...defaults,
9777
+ ...stored,
9778
+ defaultProvider: loadConfig().llm.defaultProvider,
9779
+ fallbackPolicy: loadConfig().llm.fallbackPolicy,
9780
+ fallbackOrder: [...loadConfig().llm.fallbackOrder],
9781
+ providers: {
9782
+ "claude-code": {
9783
+ ...defaults.providers["claude-code"],
9784
+ ...stored.providers?.["claude-code"] ?? {}
9785
+ },
9786
+ codex: {
9787
+ ...defaults.providers.codex,
9788
+ ...stored.providers?.codex ?? {}
9789
+ },
9790
+ ollama: {
9791
+ ...defaults.providers.ollama,
9792
+ ...stored.providers?.ollama ?? {}
9793
+ }
9794
+ }
9795
+ };
9796
+ }
9797
+ function saveLlmRuntimeState(state) {
9798
+ writeJson("llm-runtime-state.json", state);
9799
+ }
9800
+ function saveTopologyOptimizeStatus(status) {
9801
+ writeJson("topology-optimize-status.json", status);
9802
+ }
9650
9803
  function loadStoredTopologyReviewCandidates() {
9651
9804
  return readJson("topology-review-candidates.json", []).slice().sort((a, b) => b.lastObservedAt.localeCompare(a.lastObservedAt) || a.title.localeCompare(b.title));
9652
9805
  }
@@ -11559,6 +11712,40 @@ var init_data = __esm(() => {
11559
11712
 
11560
11713
  // src/utils/llm.ts
11561
11714
  import { spawn } from "node:child_process";
11715
+ import { existsSync as existsSync4, mkdtempSync, readFileSync as readFileSync4, rmSync } from "node:fs";
11716
+ import { join as join4 } from "node:path";
11717
+ import { tmpdir } from "node:os";
11718
+ function providerConfigKey2(provider) {
11719
+ if (provider === "claude-code")
11720
+ return "claudeCode";
11721
+ if (provider === "codex")
11722
+ return "codex";
11723
+ return "ollama";
11724
+ }
11725
+ function providerLabel(provider) {
11726
+ return PROVIDER_LABELS[provider];
11727
+ }
11728
+ function getProviderLabel(provider) {
11729
+ return providerLabel(provider);
11730
+ }
11731
+ function getProviderConfig(config, provider) {
11732
+ return config.llm.providers[providerConfigKey2(provider)];
11733
+ }
11734
+ function supportsCapability(provider, capability) {
11735
+ return PROVIDER_CAPABILITIES[provider].includes(capability);
11736
+ }
11737
+ function capabilityScope(capability) {
11738
+ return capability === "web-search" ? "claude-only" : "shared";
11739
+ }
11740
+ function buildSystemPrompt(system, prompt) {
11741
+ if (!system)
11742
+ return prompt;
11743
+ return `System instructions:
11744
+ ${system}
11745
+
11746
+ User request:
11747
+ ${prompt}`;
11748
+ }
11562
11749
  function buildClaudeEnv({ dropOauthToken }) {
11563
11750
  const env = { ...process.env };
11564
11751
  if (dropOauthToken) {
@@ -11566,22 +11753,71 @@ function buildClaudeEnv({ dropOauthToken }) {
11566
11753
  }
11567
11754
  return env;
11568
11755
  }
11569
- function runClaudeOnce(prompt, args, env) {
11756
+ function classifyClaudeFailure(error) {
11757
+ const message = error instanceof Error ? error.message : String(error);
11758
+ if (CLAUDE_AUTH_ERROR_PATTERNS.some((pattern) => message.includes(pattern)))
11759
+ return "auth";
11760
+ if (message.includes("timed out"))
11761
+ return "timeout";
11762
+ if (message.includes("Failed to spawn claude"))
11763
+ return "spawn";
11764
+ return "unknown";
11765
+ }
11766
+ function classifyCodexFailure(error) {
11767
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
11768
+ if (CODEX_AUTH_ERROR_PATTERNS.some((pattern) => message.includes(pattern.toLowerCase())))
11769
+ return "auth";
11770
+ if (message.includes("timed out"))
11771
+ return "timeout";
11772
+ if (message.includes("failed to spawn codex") || message.includes("enoent"))
11773
+ return "spawn";
11774
+ return "unknown";
11775
+ }
11776
+ function classifyOllamaFailure(error) {
11777
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
11778
+ if (OLLAMA_MODEL_MISSING_PATTERNS.some((pattern) => message.includes(pattern)))
11779
+ return "model-missing";
11780
+ if (message.includes("econnrefused") || message.includes("connect") || message.includes("network"))
11781
+ return "network";
11782
+ if (message.includes("timed out") || message.includes("aborterror"))
11783
+ return "timeout";
11784
+ return "unknown";
11785
+ }
11786
+ function hasBinary(command) {
11787
+ return new Promise((resolve) => {
11788
+ const proc = spawn(command, ["--help"], { stdio: "ignore" });
11789
+ let settled = false;
11790
+ proc.on("error", () => {
11791
+ if (!settled) {
11792
+ settled = true;
11793
+ resolve(false);
11794
+ }
11795
+ });
11796
+ proc.on("close", () => {
11797
+ if (!settled) {
11798
+ settled = true;
11799
+ resolve(true);
11800
+ }
11801
+ });
11802
+ });
11803
+ }
11804
+ function runProcess(command, args, options) {
11570
11805
  return new Promise((resolve, reject) => {
11571
- const proc = spawn("claude", args, {
11806
+ const proc = spawn(command, args, {
11572
11807
  stdio: ["pipe", "pipe", "pipe"],
11573
- env
11808
+ env: options?.env
11574
11809
  });
11575
11810
  let stdout = "";
11576
11811
  let stderr = "";
11577
11812
  let settled = false;
11813
+ const timeoutMs = options?.timeoutMs ?? 180000;
11578
11814
  const timeout = setTimeout(() => {
11579
11815
  proc.kill();
11580
11816
  if (!settled) {
11581
11817
  settled = true;
11582
- reject(new Error("claude call timed out after 180s"));
11818
+ reject(new Error(`${command} call timed out after ${Math.round(timeoutMs / 1000)}s`));
11583
11819
  }
11584
- }, 180000);
11820
+ }, timeoutMs);
11585
11821
  proc.stdout.on("data", (data) => {
11586
11822
  stdout += data.toString();
11587
11823
  });
@@ -11594,10 +11830,10 @@ function runClaudeOnce(prompt, args, env) {
11594
11830
  return;
11595
11831
  settled = true;
11596
11832
  if (code !== 0) {
11597
- const output = (stderr || stdout).slice(0, 500);
11598
- reject(new Error(`claude exited with code ${code}: ${output}`));
11833
+ const output = (stderr || stdout || `No output from ${command}`).slice(0, 1500);
11834
+ reject(new Error(`${command} exited with code ${code}: ${output}`));
11599
11835
  } else {
11600
- resolve(stdout.trim());
11836
+ resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
11601
11837
  }
11602
11838
  });
11603
11839
  proc.on("error", (err) => {
@@ -11605,47 +11841,630 @@ function runClaudeOnce(prompt, args, env) {
11605
11841
  if (settled)
11606
11842
  return;
11607
11843
  settled = true;
11608
- reject(new Error(`Failed to spawn claude: ${err.message}`));
11844
+ reject(new Error(`Failed to spawn ${command}: ${err.message}`));
11609
11845
  });
11610
- proc.stdin.write(prompt);
11846
+ if (options?.stdin)
11847
+ proc.stdin.write(options.stdin);
11611
11848
  proc.stdin.end();
11612
11849
  });
11613
11850
  }
11614
- function isRetryableClaudeAuthError(error) {
11615
- const message = error instanceof Error ? error.message : String(error);
11616
- return CLAUDE_AUTH_ERROR_PATTERNS.some((pattern) => message.includes(pattern));
11851
+ async function readClaudeAuthStatus(command, env) {
11852
+ try {
11853
+ const result = await runProcess(command, ["auth", "status"], { env, timeoutMs: 1e4 });
11854
+ const raw = (result.stdout || result.stderr).trim();
11855
+ if (!raw)
11856
+ return null;
11857
+ return JSON.parse(raw);
11858
+ } catch {
11859
+ return null;
11860
+ }
11861
+ }
11862
+ function summarizeAuthStatus(status) {
11863
+ if (!status)
11864
+ return "Unable to read `claude auth status` output.";
11865
+ if (status.loggedIn) {
11866
+ const method = status.authMethod ? ` via ${status.authMethod}` : "";
11867
+ const plan = status.subscriptionType ? ` (${status.subscriptionType})` : "";
11868
+ return `\`claude auth status\` reports logged in${method}${plan}, but live execution still returned unauthorized.`;
11869
+ }
11870
+ return "`claude auth status` does not report an active login.";
11871
+ }
11872
+ function buildClaudeAuthFailureMessage(params) {
11873
+ const firstMessage = params.firstError instanceof Error ? params.firstError.message : String(params.firstError);
11874
+ const finalMessage = params.finalError instanceof Error ? params.finalError.message : String(params.finalError);
11875
+ const lines = [
11876
+ "Claude execution failed because authentication is not healthy.",
11877
+ params.hadOauthToken ? "Retry without inherited `CLAUDE_CODE_OAUTH_TOKEN` was attempted, but execution still failed." : "No inherited `CLAUDE_CODE_OAUTH_TOKEN` was available to strip before failure classification.",
11878
+ summarizeAuthStatus(params.authStatus),
11879
+ `Next step: run \`${params.command} auth login\` to refresh credentials, then retry the command.`
11880
+ ];
11881
+ if (params.hadOauthToken) {
11882
+ lines.push("If you export `CLAUDE_CODE_OAUTH_TOKEN` in your shell, remove it so managed Claude auth can take over.");
11883
+ }
11884
+ lines.push("");
11885
+ lines.push(`First failure: ${firstMessage.slice(0, 500)}`);
11886
+ if (finalMessage !== firstMessage)
11887
+ lines.push(`Final failure: ${finalMessage.slice(0, 500)}`);
11888
+ return lines.join(`
11889
+ `);
11890
+ }
11891
+ function buildCodexFailureMessage(kind, error, command) {
11892
+ const baseMessage = error instanceof Error ? error.message : String(error);
11893
+ if (kind === "auth") {
11894
+ return new ProviderCommandError({
11895
+ provider: "codex",
11896
+ kind,
11897
+ message: [
11898
+ "Codex execution failed because authentication is not healthy.",
11899
+ `Next step: run \`${command} login\` to refresh Codex credentials, then retry the command.`,
11900
+ "",
11901
+ `Failure: ${baseMessage.slice(0, 500)}`
11902
+ ].join(`
11903
+ `),
11904
+ nextStep: `Run \`${command} login\` to refresh Codex credentials, then retry.`
11905
+ });
11906
+ }
11907
+ if (kind === "spawn") {
11908
+ return new ProviderCommandError({
11909
+ provider: "codex",
11910
+ kind,
11911
+ message: `Codex execution could not start. Ensure the \`${command}\` CLI is installed and available in PATH.
11912
+
11913
+ Failure: ${baseMessage.slice(0, 500)}`,
11914
+ nextStep: `Install or fix the \`${command}\` CLI, then retry.`
11915
+ });
11916
+ }
11917
+ return new ProviderCommandError({
11918
+ provider: "codex",
11919
+ kind,
11920
+ message: `Codex execution failed.
11921
+
11922
+ Failure: ${baseMessage.slice(0, 500)}`,
11923
+ nextStep: "Review the Codex CLI error and retry after fixing the provider state."
11924
+ });
11925
+ }
11926
+ function buildOllamaFailureMessage(kind, error, host, model) {
11927
+ const baseMessage = error instanceof Error ? error.message : String(error);
11928
+ if (kind === "model-missing") {
11929
+ return new ProviderCommandError({
11930
+ provider: "ollama",
11931
+ kind,
11932
+ message: `Ollama could not run model \`${model}\` because it is not available locally.
11933
+
11934
+ Failure: ${baseMessage.slice(0, 500)}`,
11935
+ nextStep: `Pull the model with \`ollama pull ${model}\` or change the configured Ollama model, then retry.`
11936
+ });
11937
+ }
11938
+ if (kind === "network") {
11939
+ return new ProviderCommandError({
11940
+ provider: "ollama",
11941
+ kind,
11942
+ message: `Ollama is not reachable at ${host}.
11943
+
11944
+ Failure: ${baseMessage.slice(0, 500)}`,
11945
+ nextStep: "Start the Ollama daemon or verify the configured host before retrying."
11946
+ });
11947
+ }
11948
+ return new ProviderCommandError({
11949
+ provider: "ollama",
11950
+ kind,
11951
+ message: `Ollama execution failed.
11952
+
11953
+ Failure: ${baseMessage.slice(0, 500)}`,
11954
+ nextStep: "Review the Ollama error, verify the daemon/model, and retry."
11955
+ });
11956
+ }
11957
+ async function runClaudeOnce(prompt, args, command, env) {
11958
+ const result = await runProcess(command, args, { stdin: prompt, env });
11959
+ return result.stdout.trim();
11617
11960
  }
11618
- async function runClaude(prompt, args) {
11961
+ async function runClaudeWithRecovery(prompt, args, command) {
11962
+ const inheritedEnv = buildClaudeEnv({ dropOauthToken: false });
11619
11963
  try {
11620
- return await runClaudeOnce(prompt, args, buildClaudeEnv({ dropOauthToken: false }));
11964
+ return await runClaudeOnce(prompt, args, command, inheritedEnv);
11621
11965
  } catch (error) {
11622
- if (!process.env.CLAUDE_CODE_OAUTH_TOKEN || !isRetryableClaudeAuthError(error)) {
11966
+ if (classifyClaudeFailure(error) !== "auth") {
11623
11967
  throw error;
11624
11968
  }
11625
- return runClaudeOnce(prompt, args, buildClaudeEnv({ dropOauthToken: true }));
11969
+ const hadOauthToken = Boolean(process.env.CLAUDE_CODE_OAUTH_TOKEN);
11970
+ const scrubbedEnv = buildClaudeEnv({ dropOauthToken: true });
11971
+ if (!hadOauthToken) {
11972
+ const authStatus = await readClaudeAuthStatus(command, scrubbedEnv);
11973
+ throw new ProviderCommandError({
11974
+ provider: "claude-code",
11975
+ kind: "auth",
11976
+ message: buildClaudeAuthFailureMessage({
11977
+ command,
11978
+ hadOauthToken,
11979
+ authStatus,
11980
+ firstError: error,
11981
+ finalError: error
11982
+ }),
11983
+ nextStep: `Run \`${command} auth login\` to refresh credentials, then retry.`,
11984
+ loggedIn: authStatus?.loggedIn,
11985
+ authMethod: authStatus?.authMethod,
11986
+ apiProvider: authStatus?.apiProvider
11987
+ });
11988
+ }
11989
+ try {
11990
+ return await runClaudeOnce(prompt, args, command, scrubbedEnv);
11991
+ } catch (retryError) {
11992
+ if (classifyClaudeFailure(retryError) !== "auth") {
11993
+ throw retryError;
11994
+ }
11995
+ const authStatus = await readClaudeAuthStatus(command, scrubbedEnv);
11996
+ throw new ProviderCommandError({
11997
+ provider: "claude-code",
11998
+ kind: "auth",
11999
+ message: buildClaudeAuthFailureMessage({
12000
+ command,
12001
+ hadOauthToken,
12002
+ authStatus,
12003
+ firstError: error,
12004
+ finalError: retryError
12005
+ }),
12006
+ nextStep: `Run \`${command} auth login\` to refresh credentials, then retry.`,
12007
+ loggedIn: authStatus?.loggedIn,
12008
+ authMethod: authStatus?.authMethod,
12009
+ apiProvider: authStatus?.apiProvider
12010
+ });
12011
+ }
11626
12012
  }
11627
12013
  }
11628
- async function chat(options) {
11629
- const config = loadConfig();
11630
- const model = options.model ?? config.model;
12014
+ async function runClaudeCapability(request, config) {
12015
+ const providerConfig = config.llm.providers.claudeCode;
12016
+ if (!providerConfig.enabled) {
12017
+ throw new ProviderCommandError({
12018
+ provider: "claude-code",
12019
+ kind: "not-configured",
12020
+ message: "Claude Code is disabled in ~/.helix/config.json.",
12021
+ nextStep: "Enable Claude Code in the Helix config or choose another provider for shared capabilities."
12022
+ });
12023
+ }
12024
+ const command = providerConfig.command;
12025
+ const model = request.model ?? (request.capability === "judge" ? providerConfig.judgeModel : providerConfig.model);
12026
+ if (request.capability === "web-search") {
12027
+ const args2 = [
12028
+ "--print",
12029
+ "--allowedTools",
12030
+ "WebSearch,WebFetch",
12031
+ "--no-session-persistence",
12032
+ "--dangerously-skip-permissions",
12033
+ "--max-turns",
12034
+ "8"
12035
+ ];
12036
+ if (model)
12037
+ args2.push("--model", model);
12038
+ return runClaudeWithRecovery(request.prompt, args2, command);
12039
+ }
11631
12040
  const args = [
11632
12041
  "--print",
11633
- "--model",
11634
- model,
11635
12042
  "--output-format",
11636
12043
  "text",
11637
12044
  "--no-session-persistence"
11638
12045
  ];
11639
- if (options.system) {
11640
- args.push("--system-prompt", options.system);
12046
+ if (model)
12047
+ args.push("--model", model);
12048
+ if (request.system)
12049
+ args.push("--system-prompt", request.system);
12050
+ return runClaudeWithRecovery(request.prompt, args, command);
12051
+ }
12052
+ async function runCodexCapability(request, config) {
12053
+ const providerConfig = config.llm.providers.codex;
12054
+ if (!providerConfig.enabled) {
12055
+ throw new ProviderCommandError({
12056
+ provider: "codex",
12057
+ kind: "not-configured",
12058
+ message: "Codex is disabled in ~/.helix/config.json.",
12059
+ nextStep: "Enable Codex in the Helix config before selecting it."
12060
+ });
12061
+ }
12062
+ const tempDir = mkdtempSync(join4(tmpdir(), "helix-codex-"));
12063
+ const outputPath = join4(tempDir, "last-message.txt");
12064
+ const model = request.model ?? (request.capability === "judge" ? providerConfig.judgeModel : providerConfig.model);
12065
+ const prompt = buildSystemPrompt(request.system, request.prompt);
12066
+ const args = [
12067
+ "exec",
12068
+ "-c",
12069
+ 'model_reasoning_effort="high"',
12070
+ "--skip-git-repo-check",
12071
+ "--sandbox",
12072
+ "read-only",
12073
+ "--output-last-message",
12074
+ outputPath,
12075
+ "--color",
12076
+ "never"
12077
+ ];
12078
+ if (model)
12079
+ args.push("--model", model);
12080
+ args.push(prompt);
12081
+ try {
12082
+ await runProcess(providerConfig.command, args, { timeoutMs: 180000 });
12083
+ if (!existsSync4(outputPath)) {
12084
+ throw new Error("codex finished without writing an output file");
12085
+ }
12086
+ return readFileSync4(outputPath, "utf-8").trim();
12087
+ } catch (error) {
12088
+ throw buildCodexFailureMessage(classifyCodexFailure(error), error, providerConfig.command);
12089
+ } finally {
12090
+ rmSync(tempDir, { recursive: true, force: true });
12091
+ }
12092
+ }
12093
+ async function runOllamaCapability(request, config) {
12094
+ const providerConfig = config.llm.providers.ollama;
12095
+ if (!providerConfig.enabled) {
12096
+ throw new ProviderCommandError({
12097
+ provider: "ollama",
12098
+ kind: "not-configured",
12099
+ message: "Ollama is disabled in ~/.helix/config.json.",
12100
+ nextStep: "Enable Ollama in the Helix config before selecting it."
12101
+ });
12102
+ }
12103
+ const model = request.model ?? (request.capability === "judge" ? providerConfig.judgeModel : providerConfig.model);
12104
+ const prompt = buildSystemPrompt(request.system, request.prompt);
12105
+ try {
12106
+ const response = await fetch(`${providerConfig.host.replace(/\/$/, "")}/api/generate`, {
12107
+ method: "POST",
12108
+ headers: { "Content-Type": "application/json" },
12109
+ body: JSON.stringify({
12110
+ model,
12111
+ prompt,
12112
+ stream: false
12113
+ }),
12114
+ signal: AbortSignal.timeout(180000)
12115
+ });
12116
+ const raw = await response.text();
12117
+ if (!response.ok) {
12118
+ throw new Error(raw || `ollama request failed with status ${response.status}`);
12119
+ }
12120
+ const parsed = JSON.parse(raw);
12121
+ if (parsed.error)
12122
+ throw new Error(parsed.error);
12123
+ return (parsed.response ?? "").trim();
12124
+ } catch (error) {
12125
+ throw buildOllamaFailureMessage(classifyOllamaFailure(error), error, providerConfig.host, model);
12126
+ }
12127
+ }
12128
+ function initializeSnapshot(state, provider, config) {
12129
+ const providerConfig = getProviderConfig(config, provider);
12130
+ return {
12131
+ ...state.providers[provider],
12132
+ provider,
12133
+ configured: providerConfig.enabled,
12134
+ enabled: providerConfig.enabled,
12135
+ model: providerConfig.model,
12136
+ judgeModel: providerConfig.judgeModel,
12137
+ notices: state.providers[provider]?.notices ?? []
12138
+ };
12139
+ }
12140
+ function saveRuntimeMutation(mutator) {
12141
+ const state = loadLlmRuntimeState();
12142
+ mutator(state);
12143
+ state.updatedAt = new Date().toISOString();
12144
+ saveLlmRuntimeState(state);
12145
+ return state;
12146
+ }
12147
+ function updateSnapshotOnFailure(params) {
12148
+ saveRuntimeMutation((state) => {
12149
+ const ranAt = new Date().toISOString();
12150
+ const snapshot = initializeSnapshot(state, params.provider, params.config);
12151
+ snapshot.installed = params.error.kind !== "spawn" && params.error.kind !== "not-installed" ? true : snapshot.installed;
12152
+ snapshot.available = false;
12153
+ snapshot.status = params.error.kind === "not-configured" ? "unavailable" : "degraded";
12154
+ snapshot.summary = params.error.warning ?? params.error.message.split(`
12155
+ `)[0] ?? `${providerLabel(params.provider)} failed.`;
12156
+ snapshot.nextStep = params.error.nextStep;
12157
+ snapshot.failureKind = params.error.kind;
12158
+ snapshot.warning = params.error.warning;
12159
+ snapshot.lastCheckedAt = ranAt;
12160
+ snapshot.lastSelectedAt = ranAt;
12161
+ snapshot.lastFailedAt = ranAt;
12162
+ snapshot.lastFailureMessage = params.error.message.slice(0, 600);
12163
+ snapshot.notices = [...params.error.notices];
12164
+ if (params.error.loggedIn !== undefined)
12165
+ snapshot.loggedIn = params.error.loggedIn;
12166
+ if (params.error.authMethod)
12167
+ snapshot.authMethod = params.error.authMethod;
12168
+ if (params.error.apiProvider)
12169
+ snapshot.apiProvider = params.error.apiProvider;
12170
+ state.providers[params.provider] = snapshot;
12171
+ });
12172
+ }
12173
+ function updateSnapshotOnSuccess(params) {
12174
+ saveRuntimeMutation((state) => {
12175
+ const ranAt = new Date().toISOString();
12176
+ const snapshot = initializeSnapshot(state, params.provider, params.config);
12177
+ snapshot.installed = true;
12178
+ snapshot.available = true;
12179
+ snapshot.status = params.warning ? "degraded" : "healthy";
12180
+ snapshot.summary = params.summary;
12181
+ snapshot.warning = params.warning;
12182
+ snapshot.failureKind = undefined;
12183
+ snapshot.nextStep = undefined;
12184
+ snapshot.lastCheckedAt = ranAt;
12185
+ snapshot.lastSelectedAt = ranAt;
12186
+ snapshot.lastUsedAt = ranAt;
12187
+ snapshot.lastSucceededAt = ranAt;
12188
+ snapshot.lastFailureMessage = undefined;
12189
+ snapshot.notices = params.warning ? [params.warning] : [];
12190
+ state.providers[params.provider] = snapshot;
12191
+ });
12192
+ }
12193
+ function recordExecution(params) {
12194
+ saveRuntimeMutation((state) => {
12195
+ state.defaultProvider = params.config.llm.defaultProvider;
12196
+ state.fallbackPolicy = params.config.llm.fallbackPolicy;
12197
+ state.fallbackOrder = [...params.config.llm.fallbackOrder];
12198
+ state.lastExecution = {
12199
+ capability: params.capability,
12200
+ scope: params.scope,
12201
+ selectionMode: params.selectionMode,
12202
+ selectedProvider: params.selectedProvider,
12203
+ attemptedProviders: [...params.attemptedProviders],
12204
+ usedProvider: params.usedProvider,
12205
+ fallbackUsed: params.fallbackUsed,
12206
+ success: params.success,
12207
+ ranAt: new Date().toISOString(),
12208
+ summary: params.summary,
12209
+ nextStep: params.nextStep,
12210
+ failureKind: params.failureKind,
12211
+ warning: params.warning
12212
+ };
12213
+ });
12214
+ }
12215
+ async function executeProvider(provider, request, config) {
12216
+ if (!supportsCapability(provider, request.capability)) {
12217
+ throw new ProviderCommandError({
12218
+ provider,
12219
+ kind: "unknown",
12220
+ message: `${providerLabel(provider)} does not support the ${request.capability} capability in HelixEvo yet.`,
12221
+ nextStep: request.capability === "web-search" ? "Use Claude Code for Claude-scoped web-search and research operations." : "Select a provider that supports this capability or keep Claude Code as the default."
12222
+ });
12223
+ }
12224
+ const providerConfig = getProviderConfig(config, provider);
12225
+ const binaryRequired = provider !== "ollama";
12226
+ if (binaryRequired && !await hasBinary(providerConfig.command)) {
12227
+ throw new ProviderCommandError({
12228
+ provider,
12229
+ kind: "not-installed",
12230
+ message: `${providerLabel(provider)} is not available because the \`${providerConfig.command}\` command could not be found.`,
12231
+ nextStep: `Install or fix the ${providerLabel(provider)} CLI, then retry.`
12232
+ });
12233
+ }
12234
+ const text = provider === "claude-code" ? await runClaudeCapability(request, config) : provider === "codex" ? await runCodexCapability(request, config) : await runOllamaCapability(request, config);
12235
+ return { text, provider };
12236
+ }
12237
+ function buildProviderSummary(params) {
12238
+ const capability = params.capability === "web-search" ? "Claude-scoped web search" : params.capability;
12239
+ if (params.fallbackUsed) {
12240
+ return `${capability} used fallback provider ${providerLabel(params.provider)} after ${providerLabel(params.selectedProvider)} degraded.`;
12241
+ }
12242
+ return `${capability} used ${providerLabel(params.provider)} successfully.`;
12243
+ }
12244
+ async function runWithProviderControl(request) {
12245
+ const config = loadConfig();
12246
+ const scope = capabilityScope(request.capability);
12247
+ const selectedProvider = scope === "claude-only" ? "claude-code" : request.provider ?? config.llm.defaultProvider;
12248
+ const selectionMode = request.provider ? "explicit" : "default";
12249
+ const allowFallback = scope === "shared" && request.allowFallback !== false && config.llm.fallbackPolicy === "on-failure";
12250
+ const attemptedProviders = [selectedProvider];
12251
+ if (allowFallback) {
12252
+ for (const provider of config.llm.fallbackOrder) {
12253
+ if (provider !== selectedProvider && supportsCapability(provider, request.capability))
12254
+ attemptedProviders.push(provider);
12255
+ }
12256
+ }
12257
+ let lastError = null;
12258
+ for (const provider of attemptedProviders) {
12259
+ try {
12260
+ const result = await executeProvider(provider, request, config);
12261
+ const fallbackUsed = provider !== selectedProvider;
12262
+ const summary2 = buildProviderSummary({
12263
+ provider,
12264
+ capability: request.capability,
12265
+ selectedProvider,
12266
+ fallbackUsed,
12267
+ attemptedProviders
12268
+ });
12269
+ updateSnapshotOnSuccess({
12270
+ provider,
12271
+ config,
12272
+ summary: summary2,
12273
+ warning: fallbackUsed ? `Fallback used after ${providerLabel(selectedProvider)} degraded.` : undefined
12274
+ });
12275
+ recordExecution({
12276
+ config,
12277
+ capability: request.capability,
12278
+ scope,
12279
+ selectionMode,
12280
+ selectedProvider,
12281
+ attemptedProviders,
12282
+ usedProvider: provider,
12283
+ fallbackUsed,
12284
+ success: true,
12285
+ summary: summary2,
12286
+ warning: fallbackUsed ? `Fallback used after ${providerLabel(selectedProvider)} degraded.` : undefined
12287
+ });
12288
+ return result.text;
12289
+ } catch (error) {
12290
+ const providerError = error instanceof ProviderCommandError ? error : new ProviderCommandError({
12291
+ provider,
12292
+ kind: provider === "claude-code" ? classifyClaudeFailure(error) : provider === "codex" ? classifyCodexFailure(error) : classifyOllamaFailure(error),
12293
+ message: error instanceof Error ? error.message : String(error)
12294
+ });
12295
+ updateSnapshotOnFailure({ provider, config, error: providerError });
12296
+ lastError = providerError;
12297
+ }
12298
+ }
12299
+ const failure = lastError ?? new ProviderCommandError({
12300
+ provider: selectedProvider,
12301
+ kind: "unknown",
12302
+ message: `${providerLabel(selectedProvider)} failed without a classified provider error.`
12303
+ });
12304
+ const fallbackAttempted = attemptedProviders.length > 1;
12305
+ const summary = failure.kind === "not-configured" ? `${providerLabel(selectedProvider)} is not enabled for this HelixEvo capability.` : fallbackAttempted ? `${providerLabel(selectedProvider)} degraded and no configured fallback succeeded.` : `${providerLabel(selectedProvider)} degraded and no fallback was used.`;
12306
+ recordExecution({
12307
+ config,
12308
+ capability: request.capability,
12309
+ scope,
12310
+ selectionMode,
12311
+ selectedProvider,
12312
+ attemptedProviders,
12313
+ fallbackUsed: false,
12314
+ success: false,
12315
+ summary,
12316
+ nextStep: failure.nextStep,
12317
+ failureKind: failure.kind,
12318
+ warning: failure.warning
12319
+ });
12320
+ throw failure;
12321
+ }
12322
+ async function probeClaudeProvider(config) {
12323
+ const providerConfig = config.llm.providers.claudeCode;
12324
+ const installed = await hasBinary(providerConfig.command);
12325
+ if (!installed) {
12326
+ return {
12327
+ provider: "claude-code",
12328
+ configured: providerConfig.enabled,
12329
+ enabled: providerConfig.enabled,
12330
+ installed: false,
12331
+ available: false,
12332
+ status: "unavailable",
12333
+ model: providerConfig.model,
12334
+ judgeModel: providerConfig.judgeModel,
12335
+ summary: `Claude Code is unavailable because the \`${providerConfig.command}\` CLI is missing.`,
12336
+ nextStep: `Install or restore the \`${providerConfig.command}\` CLI.`,
12337
+ failureKind: "not-installed",
12338
+ notices: [],
12339
+ lastCheckedAt: new Date().toISOString()
12340
+ };
12341
+ }
12342
+ const authStatus = await readClaudeAuthStatus(providerConfig.command, buildClaudeEnv({ dropOauthToken: true }));
12343
+ const runtime = loadLlmRuntimeState();
12344
+ const previous = runtime.providers["claude-code"];
12345
+ const hasHealthyExecution = previous.lastSucceededAt && (!previous.lastFailedAt || previous.lastSucceededAt >= previous.lastFailedAt);
12346
+ const status = hasHealthyExecution ? "healthy" : authStatus?.loggedIn ? "unknown" : "degraded";
12347
+ return {
12348
+ ...previous,
12349
+ provider: "claude-code",
12350
+ configured: providerConfig.enabled,
12351
+ enabled: providerConfig.enabled,
12352
+ installed: true,
12353
+ available: Boolean(authStatus?.loggedIn),
12354
+ loggedIn: authStatus?.loggedIn,
12355
+ authMethod: authStatus?.authMethod,
12356
+ apiProvider: authStatus?.apiProvider,
12357
+ status,
12358
+ model: providerConfig.model,
12359
+ judgeModel: providerConfig.judgeModel,
12360
+ summary: hasHealthyExecution ? previous.summary : authStatus?.loggedIn ? "Claude Code is installed and logged in, but no healthy live execution has been recorded yet." : "Claude Code is installed, but login/auth health is not currently confirmed.",
12361
+ nextStep: hasHealthyExecution ? previous.nextStep : authStatus?.loggedIn ? "Run a Claude-backed command to confirm live execution health." : `Run \`${providerConfig.command} auth login\` to refresh Claude auth, then retry.`,
12362
+ lastCheckedAt: new Date().toISOString(),
12363
+ notices: previous.notices ?? []
12364
+ };
12365
+ }
12366
+ async function probeCodexProvider(config) {
12367
+ const providerConfig = config.llm.providers.codex;
12368
+ const installed = await hasBinary(providerConfig.command);
12369
+ const runtime = loadLlmRuntimeState();
12370
+ const previous = runtime.providers.codex;
12371
+ return {
12372
+ ...previous,
12373
+ provider: "codex",
12374
+ configured: providerConfig.enabled,
12375
+ enabled: providerConfig.enabled,
12376
+ installed,
12377
+ available: installed && providerConfig.enabled,
12378
+ status: !providerConfig.enabled ? "unknown" : installed ? previous.status === "healthy" || previous.status === "degraded" ? previous.status : "unknown" : "unavailable",
12379
+ model: providerConfig.model,
12380
+ judgeModel: providerConfig.judgeModel,
12381
+ summary: !providerConfig.enabled ? "GPT Codex is configured as an optional provider but currently disabled." : installed ? previous.summary || "GPT Codex is installed. No live Codex execution has been recorded yet." : `GPT Codex is unavailable because the \`${providerConfig.command}\` CLI is missing.`,
12382
+ nextStep: !providerConfig.enabled ? "Enable Codex in ~/.helix/config.json before selecting it." : installed ? previous.nextStep || "Run a Codex-backed command to record live execution health." : `Install or restore the \`${providerConfig.command}\` CLI.`,
12383
+ failureKind: installed ? previous.failureKind : "not-installed",
12384
+ lastCheckedAt: new Date().toISOString(),
12385
+ notices: previous.notices ?? []
12386
+ };
12387
+ }
12388
+ async function probeOllamaProvider(config) {
12389
+ const providerConfig = config.llm.providers.ollama;
12390
+ const runtime = loadLlmRuntimeState();
12391
+ const previous = runtime.providers.ollama;
12392
+ let available = false;
12393
+ let summary = previous.summary || "Ollama has not been probed yet.";
12394
+ let nextStep = previous.nextStep;
12395
+ let failureKind = previous.failureKind;
12396
+ let status = previous.status;
12397
+ const notices = [...previous.notices ?? []];
12398
+ try {
12399
+ const response = await fetch(`${providerConfig.host.replace(/\/$/, "")}/api/tags`, {
12400
+ signal: AbortSignal.timeout(8000)
12401
+ });
12402
+ available = response.ok;
12403
+ status = available ? previous.status === "healthy" || previous.status === "degraded" ? previous.status : "unknown" : "degraded";
12404
+ summary = available ? previous.summary || "Ollama is reachable. No live Ollama execution has been recorded yet." : `Ollama responded from ${providerConfig.host} but did not report a healthy tags endpoint.`;
12405
+ nextStep = available ? previous.nextStep || "Run an Ollama-backed command to record live execution health." : "Check the Ollama daemon and configured host before retrying.";
12406
+ failureKind = available ? previous.failureKind : "network";
12407
+ } catch (error) {
12408
+ available = false;
12409
+ status = providerConfig.enabled ? "degraded" : "unknown";
12410
+ summary = `Ollama is not reachable at ${providerConfig.host}.`;
12411
+ nextStep = "Start the Ollama daemon or correct the configured host, then retry.";
12412
+ failureKind = classifyOllamaFailure(error);
12413
+ notices.push(error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200));
11641
12414
  }
11642
- return runClaude(options.prompt, args);
12415
+ return {
12416
+ ...previous,
12417
+ provider: "ollama",
12418
+ configured: providerConfig.enabled,
12419
+ enabled: providerConfig.enabled,
12420
+ installed: true,
12421
+ available,
12422
+ status,
12423
+ model: providerConfig.model,
12424
+ judgeModel: providerConfig.judgeModel,
12425
+ summary: !providerConfig.enabled ? "Ollama is configured as an optional provider but currently disabled." : summary,
12426
+ nextStep: !providerConfig.enabled ? "Enable Ollama in ~/.helix/config.json before selecting it." : nextStep,
12427
+ failureKind: !providerConfig.enabled ? undefined : failureKind,
12428
+ lastCheckedAt: new Date().toISOString(),
12429
+ notices
12430
+ };
12431
+ }
12432
+ async function refreshLlmRuntimeState() {
12433
+ const config = loadConfig();
12434
+ const claude = await probeClaudeProvider(config);
12435
+ const codex = await probeCodexProvider(config);
12436
+ const ollama = await probeOllamaProvider(config);
12437
+ return saveRuntimeMutation((state) => {
12438
+ state.defaultProvider = config.llm.defaultProvider;
12439
+ state.fallbackPolicy = config.llm.fallbackPolicy;
12440
+ state.fallbackOrder = [...config.llm.fallbackOrder];
12441
+ state.providers["claude-code"] = claude;
12442
+ state.providers.codex = codex;
12443
+ state.providers.ollama = ollama;
12444
+ });
12445
+ }
12446
+ function formatClaudeCommandFailure(context, error) {
12447
+ const message = error instanceof Error ? error.message : String(error);
12448
+ return `${context}
12449
+ ${message}`;
12450
+ }
12451
+ async function chat(options) {
12452
+ return runWithProviderControl({
12453
+ ...options,
12454
+ capability: "chat",
12455
+ allowFallback: options.allowFallback
12456
+ });
11643
12457
  }
11644
12458
  async function chatJson(options) {
11645
12459
  const enrichedPrompt = options.prompt + `
11646
12460
 
11647
12461
  Respond with valid JSON only. No markdown, no explanation, just the JSON object.`;
11648
- const text = await chat({ ...options, prompt: enrichedPrompt });
12462
+ const text = await runWithProviderControl({
12463
+ ...options,
12464
+ prompt: enrichedPrompt,
12465
+ capability: options.provider ? "json" : options.model ? "judge" : "json",
12466
+ allowFallback: options.allowFallback
12467
+ });
11649
12468
  const candidates = [
11650
12469
  text.trim(),
11651
12470
  text.match(/^```(?:json)?\s*\n([\s\S]*)\n```\s*$/m)?.[1]?.trim(),
@@ -11665,24 +12484,69 @@ Respond with valid JSON only. No markdown, no explanation, just the JSON object.
11665
12484
  ${text.slice(0, 500)}`);
11666
12485
  }
11667
12486
  async function searchWeb(query) {
11668
- const args = [
11669
- "--print",
11670
- "--allowedTools",
11671
- "WebSearch,WebFetch",
11672
- "--no-session-persistence",
11673
- "--dangerously-skip-permissions",
11674
- "--max-turns",
11675
- "8"
11676
- ];
11677
- return runClaude(query, args);
12487
+ return runWithProviderControl({
12488
+ prompt: query,
12489
+ capability: "web-search",
12490
+ provider: "claude-code",
12491
+ allowFallback: false
12492
+ });
11678
12493
  }
11679
- var CLAUDE_AUTH_ERROR_PATTERNS;
12494
+ var ProviderCommandError, PROVIDER_LABELS, PROVIDER_CAPABILITIES, CLAUDE_AUTH_ERROR_PATTERNS, CODEX_AUTH_ERROR_PATTERNS, OLLAMA_MODEL_MISSING_PATTERNS;
11680
12495
  var init_llm = __esm(() => {
11681
12496
  init_config();
12497
+ init_data();
12498
+ ProviderCommandError = class ProviderCommandError extends Error {
12499
+ provider;
12500
+ kind;
12501
+ nextStep;
12502
+ warning;
12503
+ notices;
12504
+ loggedIn;
12505
+ authMethod;
12506
+ apiProvider;
12507
+ constructor(params) {
12508
+ super(params.message);
12509
+ this.name = "ProviderCommandError";
12510
+ this.provider = params.provider;
12511
+ this.kind = params.kind;
12512
+ this.nextStep = params.nextStep;
12513
+ this.warning = params.warning;
12514
+ this.notices = params.notices ?? [];
12515
+ this.loggedIn = params.loggedIn;
12516
+ this.authMethod = params.authMethod;
12517
+ this.apiProvider = params.apiProvider;
12518
+ }
12519
+ };
12520
+ PROVIDER_LABELS = {
12521
+ "claude-code": "Claude Code",
12522
+ codex: "GPT Codex",
12523
+ ollama: "Ollama"
12524
+ };
12525
+ PROVIDER_CAPABILITIES = {
12526
+ "claude-code": ["chat", "json", "judge", "web-search"],
12527
+ codex: ["chat", "json", "judge"],
12528
+ ollama: ["chat", "json", "judge"]
12529
+ };
11682
12530
  CLAUDE_AUTH_ERROR_PATTERNS = [
11683
12531
  "OAuth token has expired",
11684
12532
  "authentication_error",
11685
- "Failed to authenticate"
12533
+ "Failed to authenticate",
12534
+ "Invalid authentication credentials",
12535
+ "Not logged in",
12536
+ "Please run /login"
12537
+ ];
12538
+ CODEX_AUTH_ERROR_PATTERNS = [
12539
+ "Please run `codex login`",
12540
+ "Please run codex login",
12541
+ "authentication",
12542
+ "unauthorized",
12543
+ "401",
12544
+ "login"
12545
+ ];
12546
+ OLLAMA_MODEL_MISSING_PATTERNS = [
12547
+ "model not found",
12548
+ "pull model",
12549
+ "not found, try pulling it first"
11686
12550
  ];
11687
12551
  });
11688
12552
 
@@ -11924,9 +12788,9 @@ init_config();
11924
12788
  init_skills();
11925
12789
  init_data();
11926
12790
  init_llm();
11927
- import { join as join5 } from "node:path";
12791
+ import { join as join6 } from "node:path";
11928
12792
  import { homedir as homedir2 } from "node:os";
11929
- import { existsSync as existsSync4, cpSync } from "node:fs";
12793
+ import { existsSync as existsSync5, cpSync } from "node:fs";
11930
12794
 
11931
12795
  // src/prompts/test-gen.ts
11932
12796
  function buildTestGenPrompt(skill) {
@@ -11955,10 +12819,10 @@ Return JSON:
11955
12819
 
11956
12820
  // src/version.ts
11957
12821
  import { createRequire as createRequire2 } from "node:module";
11958
- import { join as join4, dirname as dirname2 } from "node:path";
12822
+ import { join as join5, dirname as dirname2 } from "node:path";
11959
12823
  import { fileURLToPath } from "node:url";
11960
12824
  var require2 = createRequire2(import.meta.url);
11961
- var pkg = require2(join4(dirname2(fileURLToPath(import.meta.url)), "..", "package.json"));
12825
+ var pkg = require2(join5(dirname2(fileURLToPath(import.meta.url)), "..", "package.json"));
11962
12826
  var VERSION = pkg.version;
11963
12827
 
11964
12828
  // src/commands/init.ts
@@ -11969,26 +12833,26 @@ async function initCommand(options) {
11969
12833
  const generalDir = getGeneralSkillsPath();
11970
12834
  ensureDir(sgDir);
11971
12835
  ensureDir(generalDir);
11972
- if (!existsSync4(join5(sgDir, "config.json"))) {
12836
+ if (!existsSync5(join6(sgDir, "config.json"))) {
11973
12837
  saveConfig(DEFAULT_CONFIG);
11974
12838
  console.log(" ✓ Created config.json");
11975
12839
  }
11976
12840
  const defaultPaths = [
11977
- join5(homedir2(), ".agents", "skills"),
11978
- join5(homedir2(), ".craft-agent", "workspaces")
12841
+ join6(homedir2(), ".agents", "skills"),
12842
+ join6(homedir2(), ".craft-agent", "workspaces")
11979
12843
  ];
11980
12844
  const scanPaths = options.skillsPaths ?? defaultPaths;
11981
12845
  const expandedPaths = [];
11982
12846
  for (const p of scanPaths) {
11983
- if (p.includes("workspaces") && existsSync4(p)) {
12847
+ if (p.includes("workspaces") && existsSync5(p)) {
11984
12848
  const { readdirSync: readdirSync3 } = await import("node:fs");
11985
12849
  const workspaces = readdirSync3(p, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("."));
11986
12850
  for (const ws of workspaces) {
11987
- const skillsDir = join5(p, ws.name, "skills");
11988
- if (existsSync4(skillsDir))
12851
+ const skillsDir = join6(p, ws.name, "skills");
12852
+ if (existsSync5(skillsDir))
11989
12853
  expandedPaths.push(skillsDir);
11990
12854
  }
11991
- } else if (existsSync4(p)) {
12855
+ } else if (existsSync5(p)) {
11992
12856
  expandedPaths.push(p);
11993
12857
  }
11994
12858
  }
@@ -11997,8 +12861,8 @@ async function initCommand(options) {
11997
12861
  `);
11998
12862
  let imported = 0;
11999
12863
  for (const skill of existingSkills) {
12000
- const targetDir = join5(generalDir, skill.slug);
12001
- if (existsSync4(targetDir)) {
12864
+ const targetDir = join6(generalDir, skill.slug);
12865
+ if (existsSync5(targetDir)) {
12002
12866
  console.log(` → ${skill.slug}: already exists, skipping`);
12003
12867
  continue;
12004
12868
  }
@@ -12074,14 +12938,14 @@ async function initCommand(options) {
12074
12938
  // src/commands/capture.ts
12075
12939
  init_data();
12076
12940
  init_llm();
12077
- import { readFileSync as readFileSync4, existsSync as existsSync5 } from "node:fs";
12941
+ import { readFileSync as readFileSync5, existsSync as existsSync6 } from "node:fs";
12078
12942
  import { basename as basename2 } from "node:path";
12079
12943
  async function captureCommand(sessionPath, options) {
12080
- if (!existsSync5(sessionPath)) {
12944
+ if (!existsSync6(sessionPath)) {
12081
12945
  console.error(`Session file not found: ${sessionPath}`);
12082
12946
  process.exit(1);
12083
12947
  }
12084
- const sessionContent = readFileSync4(sessionPath, "utf-8");
12948
+ const sessionContent = readFileSync5(sessionPath, "utf-8");
12085
12949
  const sessionId = basename2(sessionPath, ".json");
12086
12950
  const maxLength = 50000;
12087
12951
  const content = sessionContent.length > maxLength ? sessionContent.slice(-maxLength) : sessionContent;
@@ -12191,7 +13055,7 @@ init_config();
12191
13055
  init_data();
12192
13056
  init_skills();
12193
13057
  init_llm();
12194
- import { join as join8 } from "node:path";
13058
+ import { join as join9 } from "node:path";
12195
13059
 
12196
13060
  // src/prompts/cluster.ts
12197
13061
  function buildClusterPrompt(failures, skills, graph) {
@@ -12601,26 +13465,26 @@ Return JSON: { "score": <number>, "reason": "<one sentence>" }`;
12601
13465
 
12602
13466
  // src/core/canary.ts
12603
13467
  init_config();
12604
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync6, cpSync as cpSync2, rmSync } from "node:fs";
12605
- import { join as join6 } from "node:path";
12606
- var CANARY_DIR = join6(getHelixDir(), "canary");
12607
- var BACKUPS_DIR = join6(getHelixDir(), "backups");
13468
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync7, cpSync as cpSync2, rmSync as rmSync2 } from "node:fs";
13469
+ import { join as join7 } from "node:path";
13470
+ var CANARY_DIR = join7(getHelixDir(), "canary");
13471
+ var BACKUPS_DIR = join7(getHelixDir(), "backups");
12608
13472
  function getRegistryPath() {
12609
- return join6(getHelixDir(), "canary-registry.json");
13473
+ return join7(getHelixDir(), "canary-registry.json");
12610
13474
  }
12611
13475
  function loadRegistry() {
12612
13476
  const path = getRegistryPath();
12613
- if (!existsSync6(path))
13477
+ if (!existsSync7(path))
12614
13478
  return { entries: [] };
12615
- return JSON.parse(readFileSync5(path, "utf-8"));
13479
+ return JSON.parse(readFileSync6(path, "utf-8"));
12616
13480
  }
12617
13481
  function saveRegistry(registry) {
12618
13482
  writeFileSync4(getRegistryPath(), JSON.stringify(registry, null, 2));
12619
13483
  }
12620
13484
  function backupSkill(skillPath, slug, version) {
12621
13485
  ensureDir(BACKUPS_DIR);
12622
- const backupPath = join6(BACKUPS_DIR, `${slug}_${version}_${Date.now()}`);
12623
- if (existsSync6(skillPath)) {
13486
+ const backupPath = join7(BACKUPS_DIR, `${slug}_${version}_${Date.now()}`);
13487
+ if (existsSync7(skillPath)) {
12624
13488
  cpSync2(skillPath, backupPath, { recursive: true });
12625
13489
  }
12626
13490
  return backupPath;
@@ -12701,7 +13565,7 @@ init_config();
12701
13565
  init_skills();
12702
13566
  init_data();
12703
13567
  init_llm();
12704
- import { join as join7 } from "node:path";
13568
+ import { join as join8 } from "node:path";
12705
13569
  async function autoGeneralize(candidates, verbose = false, dryRun = false) {
12706
13570
  const created = [];
12707
13571
  const skipped = [];
@@ -12770,7 +13634,7 @@ Keep it focused and actionable. No filler.`
12770
13634
  meta.lastEvolved = new Date().toISOString();
12771
13635
  meta.tags = [...meta.tags ?? [], "auto-generalized"];
12772
13636
  meta.enhances = candidate.sourceSkills;
12773
- const skillDir = join7(getGeneralSkillsPath(), candidate.suggestedName);
13637
+ const skillDir = join8(getGeneralSkillsPath(), candidate.suggestedName);
12774
13638
  writeSkill(skillDir, meta, content);
12775
13639
  created.push(candidate.suggestedName);
12776
13640
  console.log(` ✓ Created: ${candidate.suggestedName} (domain layer)`);
@@ -13038,7 +13902,7 @@ async function evolveCommand(options) {
13038
13902
  if (finalAccepted && !dryRun) {
13039
13903
  const { meta, content } = parseSkillMd(proposalOutput.proposedSkillMd);
13040
13904
  const skillSlug2 = proposalOutput.targetSkill ?? proposal.targetSkill;
13041
- const skillDir = join8(getGeneralSkillsPath(), skillSlug2);
13905
+ const skillDir = join9(getGeneralSkillsPath(), skillSlug2);
13042
13906
  meta.generation = generation;
13043
13907
  meta.score = avgScore;
13044
13908
  meta.lastEvolved = new Date().toISOString();
@@ -13159,16 +14023,16 @@ init_skills();
13159
14023
 
13160
14024
  // src/core/knowledge-buffer.ts
13161
14025
  init_config();
13162
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "node:fs";
13163
- import { join as join9 } from "node:path";
13164
- var BUFFER_PATH = () => join9(getHelixDir(), "knowledge-buffer.json");
13165
- var DRAFTS_DIR = () => join9(getHelixDir(), "drafts");
14026
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "node:fs";
14027
+ import { join as join10 } from "node:path";
14028
+ var BUFFER_PATH = () => join10(getHelixDir(), "knowledge-buffer.json");
14029
+ var DRAFTS_DIR = () => join10(getHelixDir(), "drafts");
13166
14030
  var MAX_DISCOVERIES = 50;
13167
14031
  var MAX_DRAFTS = 10;
13168
14032
  var DECAY_RATE = 0.1;
13169
14033
  function loadBuffer() {
13170
14034
  const path = BUFFER_PATH();
13171
- if (!existsSync7(path)) {
14035
+ if (!existsSync8(path)) {
13172
14036
  return {
13173
14037
  discoveries: [],
13174
14038
  drafts: [],
@@ -13177,7 +14041,7 @@ function loadBuffer() {
13177
14041
  decayRate: DECAY_RATE
13178
14042
  };
13179
14043
  }
13180
- return JSON.parse(readFileSync6(path, "utf-8"));
14044
+ return JSON.parse(readFileSync7(path, "utf-8"));
13181
14045
  }
13182
14046
  function saveBuffer(buffer) {
13183
14047
  writeFileSync5(BUFFER_PATH(), JSON.stringify(buffer, null, 2));
@@ -13219,9 +14083,9 @@ function saveDraft(draft) {
13219
14083
  existing.avgScore = draft.avgScore;
13220
14084
  existing.iteration++;
13221
14085
  existing.lastIterated = new Date().toISOString();
13222
- const draftDir = join9(DRAFTS_DIR(), draft.skillName);
14086
+ const draftDir = join10(DRAFTS_DIR(), draft.skillName);
13223
14087
  ensureDir(draftDir);
13224
- writeFileSync5(join9(draftDir, "SKILL.md"), draft.skillMd);
14088
+ writeFileSync5(join10(draftDir, "SKILL.md"), draft.skillMd);
13225
14089
  }
13226
14090
  } else {
13227
14091
  buffer.drafts.push({
@@ -13235,9 +14099,9 @@ function saveDraft(draft) {
13235
14099
  iteration: 1,
13236
14100
  lastIterated: new Date().toISOString()
13237
14101
  });
13238
- const draftDir = join9(DRAFTS_DIR(), draft.skillName);
14102
+ const draftDir = join10(DRAFTS_DIR(), draft.skillName);
13239
14103
  ensureDir(draftDir);
13240
- writeFileSync5(join9(draftDir, "SKILL.md"), draft.skillMd);
14104
+ writeFileSync5(join10(draftDir, "SKILL.md"), draft.skillMd);
13241
14105
  }
13242
14106
  trimAndSave(buffer);
13243
14107
  }
@@ -13285,6 +14149,16 @@ function getBufferStats() {
13285
14149
  }
13286
14150
 
13287
14151
  // src/commands/status.ts
14152
+ init_llm();
14153
+ function providerStatusBadge(status) {
14154
+ if (status === "healthy")
14155
+ return "healthy";
14156
+ if (status === "degraded")
14157
+ return "degraded";
14158
+ if (status === "unavailable")
14159
+ return "unavailable";
14160
+ return "unknown";
14161
+ }
13288
14162
  async function statusCommand() {
13289
14163
  const failures = loadFailures();
13290
14164
  const unresolved = failures.filter((f) => !f.resolved);
@@ -13293,6 +14167,7 @@ async function statusCommand() {
13293
14167
  const skillTests = loadSkillTests();
13294
14168
  const stagnation = getStagnationCount();
13295
14169
  const recentIter = getRecentIterations(7);
14170
+ const llmRuntime = await refreshLlmRuntimeState();
13296
14171
  console.log(`\uD83E\uDDEC HelixEvo Status
13297
14172
  `);
13298
14173
  console.log(` Skills: ${skills.length}`);
@@ -13317,6 +14192,29 @@ async function statusCommand() {
13317
14192
  console.log(` Drafts: ${buffer.drafts}/${buffer.maxDrafts}`);
13318
14193
  if (buffer.topDraft)
13319
14194
  console.log(` Top draft: ${buffer.topDraft}`);
14195
+ console.log(`
14196
+ Provider control:`);
14197
+ console.log(` Default provider: ${getProviderLabel(llmRuntime.defaultProvider)}`);
14198
+ console.log(` Fallback policy: ${llmRuntime.fallbackPolicy}${llmRuntime.fallbackOrder.length > 0 ? ` (${llmRuntime.fallbackOrder.map((provider) => getProviderLabel(provider)).join(" → ")})` : ""}`);
14199
+ for (const provider of ["claude-code", "codex", "ollama"]) {
14200
+ const snapshot = llmRuntime.providers[provider];
14201
+ const state = providerStatusBadge(snapshot.status);
14202
+ const login = snapshot.loggedIn === undefined ? "" : snapshot.loggedIn ? " • logged in" : " • logged out";
14203
+ const availability = snapshot.available ? "available" : snapshot.installed ? "installed" : "missing";
14204
+ console.log(` ${getProviderLabel(provider)}: ${state} • ${availability}${login}`);
14205
+ console.log(` ${snapshot.summary}`);
14206
+ if (snapshot.nextStep)
14207
+ console.log(` Next: ${snapshot.nextStep}`);
14208
+ }
14209
+ if (llmRuntime.lastExecution) {
14210
+ const execution = llmRuntime.lastExecution;
14211
+ console.log(` Last execution: ${execution.success ? "success" : "failure"} • ${getProviderLabel(execution.selectedProvider)} selected${execution.usedProvider ? ` • ${getProviderLabel(execution.usedProvider)} used` : ""}`);
14212
+ console.log(` ${execution.summary}`);
14213
+ if (execution.nextStep)
14214
+ console.log(` Next: ${execution.nextStep}`);
14215
+ } else {
14216
+ console.log(" Last execution: none recorded yet");
14217
+ }
13320
14218
  const accepted = recentIter.flatMap((i) => i.proposals).filter((p) => p.outcome === "accepted").length;
13321
14219
  const rejected = recentIter.flatMap((i) => i.proposals).filter((p) => p.outcome === "rejected").length;
13322
14220
  console.log(`
@@ -13338,7 +14236,7 @@ async function statusCommand() {
13338
14236
  init_data();
13339
14237
  init_skills();
13340
14238
  import { writeFileSync as writeFileSync6 } from "node:fs";
13341
- import { join as join10 } from "node:path";
14239
+ import { join as join11 } from "node:path";
13342
14240
  init_config();
13343
14241
  async function reportCommand(options) {
13344
14242
  const days = parseInt(options.days ?? "1");
@@ -13428,8 +14326,8 @@ async function reportCommand(options) {
13428
14326
  `;
13429
14327
  report += `- Run **helixevo proof --status** to inspect outcome attribution across interventions, topology, ontology, and evolution evidence.
13430
14328
  `;
13431
- const outputPath = options.output ?? join10(getHelixDir(), "reports", `${date}.md`);
13432
- ensureDir(join10(getHelixDir(), "reports"));
14329
+ const outputPath = options.output ?? join11(getHelixDir(), "reports", `${date}.md`);
14330
+ ensureDir(join11(getHelixDir(), "reports"));
13433
14331
  writeFileSync6(outputPath, report);
13434
14332
  console.log(report);
13435
14333
  console.log(`
@@ -13441,7 +14339,7 @@ init_config();
13441
14339
  init_skills();
13442
14340
  init_data();
13443
14341
  init_llm();
13444
- import { join as join11 } from "node:path";
14342
+ import { join as join12 } from "node:path";
13445
14343
  async function generalizeCommand(options) {
13446
14344
  const verbose = options.verbose ?? false;
13447
14345
  const dryRun = options.dryRun ?? false;
@@ -13456,7 +14354,12 @@ async function generalizeCommand(options) {
13456
14354
  return;
13457
14355
  }
13458
14356
  console.log(" Step 1: Detecting cross-skill patterns...");
13459
- const candidates = await detectGeneralizationCandidates(skills);
14357
+ let candidates;
14358
+ try {
14359
+ candidates = await detectGeneralizationCandidates(skills);
14360
+ } catch (error) {
14361
+ throw new Error(formatClaudeCommandFailure("Generalize could not detect cross-skill patterns.", error));
14362
+ }
13460
14363
  if (candidates.length === 0) {
13461
14364
  console.log(" No generalization candidates found.");
13462
14365
  return;
@@ -13480,7 +14383,12 @@ async function generalizeCommand(options) {
13480
14383
  }
13481
14384
  console.log(" Generating generalized skill...");
13482
14385
  const sourceSkills = skills.filter((s) => candidate.skills.includes(s.slug));
13483
- const generalized = await generateGeneralizedSkill(sourceSkills, candidate);
14386
+ let generalized;
14387
+ try {
14388
+ generalized = await generateGeneralizedSkill(sourceSkills, candidate);
14389
+ } catch (error) {
14390
+ throw new Error(formatClaudeCommandFailure(`Generalize detected candidate "${candidate.suggestedName}" but could not materialize the generalized skill.`, error));
14391
+ }
13484
14392
  if (verbose) {
13485
14393
  console.log(` Name: ${generalized.name}`);
13486
14394
  console.log(` Content preview: ${generalized.skillMd.slice(0, 200)}...
@@ -13516,7 +14424,7 @@ async function generalizeCommand(options) {
13516
14424
  meta.generation = 1;
13517
14425
  meta.score = candidate.confidence;
13518
14426
  meta.lastEvolved = completedAt;
13519
- const skillDir = join11(getGeneralSkillsPath(), candidate.suggestedName);
14427
+ const skillDir = join12(getGeneralSkillsPath(), candidate.suggestedName);
13520
14428
  writeSkill(skillDir, meta, content);
13521
14429
  console.log(` ✓ Created: ${candidate.suggestedName} (${candidate.suggestedLayer} layer)
13522
14430
  `);
@@ -13687,7 +14595,7 @@ Return JSON:
13687
14595
  init_data();
13688
14596
  init_skills();
13689
14597
  init_llm();
13690
- import { join as join12 } from "node:path";
14598
+ import { join as join13 } from "node:path";
13691
14599
  async function specializeCommand(options) {
13692
14600
  const verbose = options.verbose ?? false;
13693
14601
  const dryRun = options.dryRun ?? false;
@@ -13792,8 +14700,8 @@ async function specializeCommand(options) {
13792
14700
  meta.generation = 1;
13793
14701
  meta.score = candidate.confidence;
13794
14702
  meta.lastEvolved = new Date().toISOString();
13795
- const projectSkillsDir = join12(process.cwd(), ".helix", "skills");
13796
- const skillDir = join12(projectSkillsDir, candidate.suggestedName);
14703
+ const projectSkillsDir = join13(process.cwd(), ".helix", "skills");
14704
+ const skillDir = join13(projectSkillsDir, candidate.suggestedName);
13797
14705
  writeSkill(skillDir, meta, content);
13798
14706
  console.log(` ✓ Created: ${candidate.suggestedName} (project layer, parent: ${candidate.domainSkill})
13799
14707
  `);
@@ -13897,8 +14805,8 @@ Return JSON:
13897
14805
  // src/commands/graph.ts
13898
14806
  import { writeFileSync as writeFileSync8 } from "node:fs";
13899
14807
  import { execSync } from "node:child_process";
13900
- import { join as join14 } from "node:path";
13901
- import { tmpdir } from "node:os";
14808
+ import { join as join15 } from "node:path";
14809
+ import { tmpdir as tmpdir2 } from "node:os";
13902
14810
 
13903
14811
  // src/core/network.ts
13904
14812
  init_ontology();
@@ -14174,16 +15082,46 @@ async function optimizeNetwork(verbose = false) {
14174
15082
  const result = {
14175
15083
  merged: preview.merged.map((candidate) => candidate.slugs),
14176
15084
  split: preview.split.map((candidate) => candidate.slug),
14177
- conflictsResolved: []
15085
+ conflictsResolved: [],
15086
+ conflictsAttempted: 0,
15087
+ conflictsSkipped: 0,
15088
+ conflictFailures: [],
15089
+ degraded: false
14178
15090
  };
14179
15091
  const conflicts = graph.edges.filter((edge) => edge.type === "conflicts");
14180
- if (conflicts.length > 0) {
14181
- if (verbose)
14182
- console.log(` Resolving ${conflicts.length} conflict(s)...`);
14183
- for (const conflict of conflicts) {
15092
+ let authBlockedFurtherConflictAnalysis = false;
15093
+ if (conflicts.length > 0 && verbose) {
15094
+ console.log(` Resolving ${conflicts.length} conflict(s)...`);
15095
+ }
15096
+ for (const conflict of conflicts) {
15097
+ if (authBlockedFurtherConflictAnalysis) {
15098
+ result.conflictsSkipped += 1;
15099
+ continue;
15100
+ }
15101
+ result.conflictsAttempted += 1;
15102
+ try {
14184
15103
  const resolved = await resolveConflict(conflict, skills);
14185
15104
  if (resolved)
14186
15105
  result.conflictsResolved.push([conflict.from, conflict.to]);
15106
+ } catch (error) {
15107
+ const failureKind = classifyClaudeFailure(error);
15108
+ result.degraded = true;
15109
+ result.failureKind = result.failureKind ?? (failureKind === "unknown" ? "llm" : failureKind);
15110
+ result.conflictFailures.push({
15111
+ pair: [conflict.from, conflict.to],
15112
+ kind: failureKind === "unknown" ? "llm" : failureKind,
15113
+ message: error instanceof Error ? error.message : String(error)
15114
+ });
15115
+ if (failureKind === "auth") {
15116
+ authBlockedFurtherConflictAnalysis = true;
15117
+ result.conflictsSkipped += Math.max(0, conflicts.length - result.conflictsAttempted);
15118
+ result.warning = "Conflict enrichment degraded because Claude authentication failed.";
15119
+ } else if (!result.warning) {
15120
+ result.warning = "Conflict enrichment degraded before all conflict analysis completed.";
15121
+ }
15122
+ if (verbose) {
15123
+ console.log(` ! Conflict analysis degraded for ${conflict.from} ↔ ${conflict.to}`);
15124
+ }
14187
15125
  }
14188
15126
  }
14189
15127
  if (verbose) {
@@ -14238,29 +15176,29 @@ Return JSON:
14238
15176
  init_data();
14239
15177
  init_skills();
14240
15178
  import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync3 } from "node:fs";
14241
- import { join as join13 } from "node:path";
15179
+ import { join as join14 } from "node:path";
14242
15180
  function syncToObsidian(vaultPath, verbose = false) {
14243
15181
  const graph = loadSkillGraph();
14244
15182
  const skills = loadAllGeneralSkills();
14245
- const skillsDir = join13(vaultPath, "Skills");
14246
- const reportsDir = join13(vaultPath, "Reports");
15183
+ const skillsDir = join14(vaultPath, "Skills");
15184
+ const reportsDir = join14(vaultPath, "Reports");
14247
15185
  mkdirSync3(skillsDir, { recursive: true });
14248
15186
  mkdirSync3(reportsDir, { recursive: true });
14249
15187
  for (const node of graph.nodes) {
14250
15188
  const skill = skills.find((s) => s.slug === node.id);
14251
15189
  const note = generateSkillNote(node, graph.edges, skill ?? undefined);
14252
- const notePath = join13(skillsDir, `${node.id}.md`);
15190
+ const notePath = join14(skillsDir, `${node.id}.md`);
14253
15191
  writeFileSync7(notePath, note);
14254
15192
  if (verbose)
14255
15193
  console.log(` ✓ ${node.id}.md`);
14256
15194
  }
14257
15195
  const indexNote = generateIndexNote(graph);
14258
- writeFileSync7(join13(vaultPath, "HelixEvo Index.md"), indexNote);
15196
+ writeFileSync7(join14(vaultPath, "HelixEvo Index.md"), indexNote);
14259
15197
  const recent = getRecentIterations(7);
14260
15198
  if (recent.length > 0) {
14261
15199
  const report = generateEvolutionReport(recent);
14262
15200
  const date = new Date().toISOString().slice(0, 10);
14263
- writeFileSync7(join13(reportsDir, `Evolution ${date}.md`), report);
15201
+ writeFileSync7(join14(reportsDir, `Evolution ${date}.md`), report);
14264
15202
  }
14265
15203
  console.log(` ✓ Synced ${graph.nodes.length} skills to ${vaultPath}`);
14266
15204
  }
@@ -14412,7 +15350,57 @@ async function graphCommand(options) {
14412
15350
  console.log(`
14413
15351
  ${C.bold} Network Optimization${C.reset}`);
14414
15352
  console.log(` ${"─".repeat(40)}`);
14415
- const result = await optimizeNetwork(verbose);
15353
+ let topology;
15354
+ try {
15355
+ topology = refreshTopologyReviewCandidates();
15356
+ } catch (error) {
15357
+ const message = error instanceof Error ? error.message : String(error);
15358
+ saveTopologyOptimizeStatus({
15359
+ command: "graph-optimize",
15360
+ status: "failed",
15361
+ ranAt: new Date().toISOString(),
15362
+ summary: "Topology review queue refresh failed before optimization could complete.",
15363
+ nextStep: "Inspect the optimize error and retry after fixing the queue-refresh failure.",
15364
+ notices: [message],
15365
+ queue: { total: 0, open: 0, created: 0, updated: 0 },
15366
+ enrichment: { attempted: 0, resolved: 0, failed: 0, skipped: 0, degraded: false, failureKind: "unknown", warning: message }
15367
+ });
15368
+ throw new Error(`graph --optimize could not refresh the topology review queue.
15369
+ ${message}`);
15370
+ }
15371
+ console.log(` ${C.blue}↺${C.reset} Topology review queue refreshed: ${topology.total} total • ${topology.open} open`);
15372
+ console.log(` ${C.dim} ${topology.created} created • ${topology.updated} updated${C.reset}`);
15373
+ let result;
15374
+ try {
15375
+ result = await optimizeNetwork(verbose);
15376
+ } catch (error) {
15377
+ const message = error instanceof Error ? error.message : String(error);
15378
+ saveTopologyOptimizeStatus({
15379
+ command: "graph-optimize",
15380
+ status: "failed",
15381
+ ranAt: new Date().toISOString(),
15382
+ summary: "Topology review queue refreshed, but optimization failed before enrichment completed.",
15383
+ nextStep: "Review the optimize error, then retry once the underlying execution issue is fixed.",
15384
+ notices: [message],
15385
+ queue: {
15386
+ total: topology.total,
15387
+ open: topology.open,
15388
+ created: topology.created,
15389
+ updated: topology.updated
15390
+ },
15391
+ enrichment: {
15392
+ attempted: 0,
15393
+ resolved: 0,
15394
+ failed: 1,
15395
+ skipped: 0,
15396
+ degraded: true,
15397
+ failureKind: "unknown",
15398
+ warning: message
15399
+ }
15400
+ });
15401
+ throw new Error(`graph --optimize refreshed the queue but failed during enrichment.
15402
+ ${message}`);
15403
+ }
14416
15404
  if (result.merged.length > 0) {
14417
15405
  for (const [a, b] of result.merged)
14418
15406
  console.log(` ${C.yellow}⊕${C.reset} Merge: ${a} + ${b}`);
@@ -14424,12 +15412,42 @@ ${C.bold} Network Optimization${C.reset}`);
14424
15412
  if (result.conflictsResolved.length > 0) {
14425
15413
  console.log(` ${C.green}✓${C.reset} Conflicts analyzed: ${result.conflictsResolved.length}`);
14426
15414
  }
14427
- if (result.merged.length === 0 && result.split.length === 0 && result.conflictsResolved.length === 0) {
15415
+ if (result.merged.length === 0 && result.split.length === 0 && result.conflictsResolved.length === 0 && !result.degraded) {
14428
15416
  console.log(` ${C.green}✓${C.reset} Network is healthy`);
14429
15417
  }
14430
- const topology = refreshTopologyReviewCandidates();
14431
- console.log(` ${C.blue}↺${C.reset} Topology review queue refreshed: ${topology.total} total • ${topology.open} open`);
14432
- console.log(` ${C.dim} ${topology.created} created ${topology.updated} updated${C.reset}`);
15418
+ const optimizeStatus = {
15419
+ command: "graph-optimize",
15420
+ status: result.degraded ? "partial" : "healthy",
15421
+ ranAt: new Date().toISOString(),
15422
+ summary: result.degraded ? "Topology review queue refreshed, but conflict enrichment only completed partially." : "Topology review queue refreshed and conflict enrichment completed successfully.",
15423
+ nextStep: result.degraded ? "Review the queue in topology control. If the warning mentions authentication, refresh Claude auth before retrying full enrichment." : topology.open > 0 ? "Review open topology candidates and decide which ones to accept, defer, or reject." : "No structural backlog is waiting right now. Re-run optimize after more pressure or graph changes appear.",
15424
+ notices: result.degraded ? [
15425
+ "Deterministic queue refresh succeeded even though enrichment degraded.",
15426
+ result.warning ?? "Conflict enrichment did not complete fully."
15427
+ ] : ["Queue refresh and enrichment completed without degraded-mode warnings."],
15428
+ queue: {
15429
+ total: topology.total,
15430
+ open: topology.open,
15431
+ created: topology.created,
15432
+ updated: topology.updated
15433
+ },
15434
+ enrichment: {
15435
+ attempted: result.conflictsAttempted,
15436
+ resolved: result.conflictsResolved.length,
15437
+ failed: result.conflictFailures.length,
15438
+ skipped: result.conflictsSkipped,
15439
+ degraded: result.degraded,
15440
+ failureKind: result.failureKind,
15441
+ warning: result.warning
15442
+ }
15443
+ };
15444
+ saveTopologyOptimizeStatus(optimizeStatus);
15445
+ if (result.degraded) {
15446
+ console.log(` ${C.yellow}!${C.reset} Optimize finished in partial mode`);
15447
+ console.log(` ${C.dim} Queue refresh succeeded, but conflict enrichment degraded.${C.reset}`);
15448
+ if (result.warning)
15449
+ console.log(` ${C.dim} ${result.warning}${C.reset}`);
15450
+ }
14433
15451
  }
14434
15452
  if (options.obsidian) {
14435
15453
  console.log(`
@@ -14622,7 +15640,7 @@ ${mermaidCode}
14622
15640
  });
14623
15641
  </script>
14624
15642
  </body></html>`;
14625
- const htmlPath = join14(tmpdir(), "helix-network.html");
15643
+ const htmlPath = join15(tmpdir2(), "helix-network.html");
14626
15644
  writeFileSync8(htmlPath, html);
14627
15645
  execSync(`open "${htmlPath}"`);
14628
15646
  console.log(` ✓ Opened in browser`);
@@ -14687,8 +15705,8 @@ function renderScoreBar(score) {
14687
15705
  init_config();
14688
15706
  init_skills();
14689
15707
  init_llm();
14690
- import { isAbsolute, join as join15, resolve } from "node:path";
14691
- import { readFileSync as readFileSync7, existsSync as existsSync9 } from "node:fs";
15708
+ import { isAbsolute, join as join16, resolve } from "node:path";
15709
+ import { readFileSync as readFileSync8, existsSync as existsSync10 } from "node:fs";
14692
15710
  init_data();
14693
15711
  async function researchCommand(options) {
14694
15712
  const verbose = options.verbose ?? false;
@@ -14835,7 +15853,7 @@ Only include discoveries that are NOT already covered by current skills.`
14835
15853
  meta.score = result.avgScore / 10;
14836
15854
  meta.lastEvolved = new Date().toISOString();
14837
15855
  meta.tags = [...meta.tags ?? [], "research-discovered"];
14838
- const skillDir = join15(getGeneralSkillsPath(), hypothesis.skillName);
15856
+ const skillDir = join16(getGeneralSkillsPath(), hypothesis.skillName);
14839
15857
  writeSkill(skillDir, meta, content);
14840
15858
  console.log(` ✓ Created: ${hypothesis.skillName} (from research)
14841
15859
  `);
@@ -14891,9 +15909,9 @@ async function understandGoals(projectPath, skills) {
14891
15909
  const paths = projectPath ? [projectPath] : [process.cwd()];
14892
15910
  for (const p of paths) {
14893
15911
  for (const file of ["README.md", "CLAUDE.md", "package.json", ".agents/AGENTS.md"]) {
14894
- const fullPath = join15(p, file);
14895
- if (existsSync9(fullPath)) {
14896
- const content = readFileSync7(fullPath, "utf-8").slice(0, 2000);
15912
+ const fullPath = join16(p, file);
15913
+ if (existsSync10(fullPath)) {
15914
+ const content = readFileSync8(fullPath, "utf-8").slice(0, 2000);
14897
15915
  context.push(`## ${file}
14898
15916
  ${content}`);
14899
15917
  }
@@ -15053,8 +16071,8 @@ ${replay.slice(0, 800)}`
15053
16071
 
15054
16072
  // src/commands/dashboard.ts
15055
16073
  import { execSync as execSync2, spawn as spawn2 } from "node:child_process";
15056
- import { join as join17, dirname as dirname3 } from "node:path";
15057
- import { existsSync as existsSync11, cpSync as cpSync3, mkdirSync as mkdirSync5, openSync, readdirSync as readdirSync3, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync10 } from "node:fs";
16074
+ import { join as join18, dirname as dirname3 } from "node:path";
16075
+ import { existsSync as existsSync12, cpSync as cpSync3, mkdirSync as mkdirSync5, openSync, readdirSync as readdirSync3, readFileSync as readFileSync10, rmSync as rmSync3, writeFileSync as writeFileSync10 } from "node:fs";
15058
16076
  import { fileURLToPath as fileURLToPath2 } from "node:url";
15059
16077
  import { homedir as homedir4 } from "node:os";
15060
16078
  import { createServer } from "node:net";
@@ -15062,25 +16080,25 @@ init_skills();
15062
16080
  init_data();
15063
16081
 
15064
16082
  // src/utils/update-check.ts
15065
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync9, existsSync as existsSync10, mkdirSync as mkdirSync4 } from "node:fs";
15066
- import { join as join16 } from "node:path";
16083
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync11, mkdirSync as mkdirSync4 } from "node:fs";
16084
+ import { join as join17 } from "node:path";
15067
16085
  import { homedir as homedir3 } from "node:os";
15068
- var HELIX_DIR2 = join16(homedir3(), ".helix");
15069
- var CACHE_PATH = join16(HELIX_DIR2, "update-check.json");
16086
+ var HELIX_DIR2 = join17(homedir3(), ".helix");
16087
+ var CACHE_PATH = join17(HELIX_DIR2, "update-check.json");
15070
16088
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
15071
16089
  var REGISTRY_URL = "https://registry.npmjs.org/helixevo/latest";
15072
16090
  function readCache() {
15073
16091
  try {
15074
- if (!existsSync10(CACHE_PATH))
16092
+ if (!existsSync11(CACHE_PATH))
15075
16093
  return null;
15076
- return JSON.parse(readFileSync8(CACHE_PATH, "utf-8"));
16094
+ return JSON.parse(readFileSync9(CACHE_PATH, "utf-8"));
15077
16095
  } catch {
15078
16096
  return null;
15079
16097
  }
15080
16098
  }
15081
16099
  function writeCache(cache) {
15082
16100
  try {
15083
- if (!existsSync10(HELIX_DIR2))
16101
+ if (!existsSync11(HELIX_DIR2))
15084
16102
  mkdirSync4(HELIX_DIR2, { recursive: true });
15085
16103
  writeFileSync9(CACHE_PATH, JSON.stringify(cache));
15086
16104
  } catch {}
@@ -15166,11 +16184,11 @@ function printUpdateBanner(currentVersion, latestVersion) {
15166
16184
 
15167
16185
  // src/commands/dashboard.ts
15168
16186
  var __filename = "/Users/tianchichen/Documents/GitHub/helixevo/src/commands/dashboard.ts";
15169
- var HELIX_HOME_DIR = join17(homedir4(), ".helix");
15170
- var HELIX_DASHBOARD_DIR = join17(HELIX_HOME_DIR, "dashboard");
15171
- var DASHBOARD_LOG_FILE = join17(HELIX_HOME_DIR, "dashboard.log");
15172
- var DASHBOARD_PID_FILE = join17(HELIX_HOME_DIR, "dashboard.pid");
15173
- var DASHBOARD_STATE_FILE = join17(HELIX_HOME_DIR, "dashboard-state.json");
16187
+ var HELIX_HOME_DIR = join18(homedir4(), ".helix");
16188
+ var HELIX_DASHBOARD_DIR = join18(HELIX_HOME_DIR, "dashboard");
16189
+ var DASHBOARD_LOG_FILE = join18(HELIX_HOME_DIR, "dashboard.log");
16190
+ var DASHBOARD_PID_FILE = join18(HELIX_HOME_DIR, "dashboard.pid");
16191
+ var DASHBOARD_STATE_FILE = join18(HELIX_HOME_DIR, "dashboard-state.json");
15174
16192
  var DASHBOARD_SKIP_AUTO_UPDATE_ENV = "HELIXEVO_DASHBOARD_SKIP_AUTO_UPDATE";
15175
16193
  var DEFAULT_DASHBOARD_PORT = 3847;
15176
16194
  var MAX_PORT_SCAN_ATTEMPTS = 25;
@@ -15195,7 +16213,7 @@ async function dashboardCommand(options) {
15195
16213
  console.error(` cd helixevo/dashboard && npm install && npx next dev --port ${preferredPort}`);
15196
16214
  process.exit(1);
15197
16215
  }
15198
- if (!existsSync11(join17(dir, "node_modules"))) {
16216
+ if (!existsSync12(join18(dir, "node_modules"))) {
15199
16217
  console.log(" Installing dashboard dependencies...");
15200
16218
  try {
15201
16219
  execSync2("npm install --no-audit --no-fund", { cwd: dir, stdio: "inherit" });
@@ -15328,10 +16346,10 @@ function launchDashboard(dir, openBrowser, port) {
15328
16346
  \uD83D\uDD04 Restarting dashboard after update on ${getDashboardUrl(port)}...
15329
16347
  `);
15330
16348
  setTimeout(() => {
15331
- const nextCache = join17(dir, ".next");
15332
- if (existsSync11(nextCache)) {
16349
+ const nextCache = join18(dir, ".next");
16350
+ if (existsSync12(nextCache)) {
15333
16351
  try {
15334
- rmSync2(nextCache, { recursive: true });
16352
+ rmSync3(nextCache, { recursive: true });
15335
16353
  } catch {}
15336
16354
  }
15337
16355
  ensureSkillGraph();
@@ -15402,17 +16420,17 @@ function writeDashboardState(state) {
15402
16420
  }
15403
16421
  function readDashboardState() {
15404
16422
  let state = null;
15405
- if (existsSync11(DASHBOARD_STATE_FILE)) {
16423
+ if (existsSync12(DASHBOARD_STATE_FILE)) {
15406
16424
  try {
15407
- const parsed = JSON.parse(readFileSync9(DASHBOARD_STATE_FILE, "utf-8"));
16425
+ const parsed = JSON.parse(readFileSync10(DASHBOARD_STATE_FILE, "utf-8"));
15408
16426
  if (Number.isInteger(parsed.pid) && Number.isInteger(parsed.port)) {
15409
16427
  state = parsed;
15410
16428
  }
15411
16429
  } catch {}
15412
16430
  }
15413
- if (!state && existsSync11(DASHBOARD_PID_FILE)) {
16431
+ if (!state && existsSync12(DASHBOARD_PID_FILE)) {
15414
16432
  try {
15415
- const pid = Number.parseInt(readFileSync9(DASHBOARD_PID_FILE, "utf-8").trim(), 10);
16433
+ const pid = Number.parseInt(readFileSync10(DASHBOARD_PID_FILE, "utf-8").trim(), 10);
15416
16434
  if (Number.isInteger(pid)) {
15417
16435
  state = {
15418
16436
  pid,
@@ -15433,10 +16451,10 @@ function readDashboardState() {
15433
16451
  }
15434
16452
  function clearDashboardState() {
15435
16453
  try {
15436
- rmSync2(DASHBOARD_PID_FILE, { force: true });
16454
+ rmSync3(DASHBOARD_PID_FILE, { force: true });
15437
16455
  } catch {}
15438
16456
  try {
15439
- rmSync2(DASHBOARD_STATE_FILE, { force: true });
16457
+ rmSync3(DASHBOARD_STATE_FILE, { force: true });
15440
16458
  } catch {}
15441
16459
  }
15442
16460
  function isProcessAlive(pid) {
@@ -15474,20 +16492,20 @@ function prepareDashboard() {
15474
16492
  const npmSource = findNpmDashboard();
15475
16493
  if (npmSource)
15476
16494
  return copyToHelix(npmSource);
15477
- if (existsSync11(join17(HELIX_DASHBOARD_DIR, "package.json"))) {
16495
+ if (existsSync12(join18(HELIX_DASHBOARD_DIR, "package.json"))) {
15478
16496
  return HELIX_DASHBOARD_DIR;
15479
16497
  }
15480
16498
  return null;
15481
16499
  }
15482
16500
  function findDevDashboard() {
15483
16501
  const candidates = [
15484
- join17(process.cwd(), "dashboard")
16502
+ join18(process.cwd(), "dashboard")
15485
16503
  ];
15486
16504
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
15487
- candidates.push(join17(home, "Documents", "GitHub", "helixevo", "dashboard"));
15488
- candidates.push(join17(home, "Documents", "GitHub", "skillgraph", "dashboard"));
16505
+ candidates.push(join18(home, "Documents", "GitHub", "helixevo", "dashboard"));
16506
+ candidates.push(join18(home, "Documents", "GitHub", "skillgraph", "dashboard"));
15489
16507
  for (const dir of candidates) {
15490
- if (existsSync11(join17(dir, "package.json")) && !dir.includes("node_modules")) {
16508
+ if (existsSync12(join18(dir, "package.json")) && !dir.includes("node_modules")) {
15491
16509
  return dir;
15492
16510
  }
15493
16511
  }
@@ -15498,15 +16516,15 @@ function findNpmDashboard() {
15498
16516
  try {
15499
16517
  const thisFile = typeof __filename !== "undefined" ? __filename : fileURLToPath2(import.meta.url);
15500
16518
  const pkgRoot = dirname3(dirname3(thisFile));
15501
- candidates.push(join17(pkgRoot, "dashboard"));
16519
+ candidates.push(join18(pkgRoot, "dashboard"));
15502
16520
  } catch {}
15503
16521
  try {
15504
16522
  const globalPrefix = execSync2("npm prefix -g", { encoding: "utf-8" }).trim();
15505
- candidates.push(join17(globalPrefix, "lib", "node_modules", "helixevo", "dashboard"));
15506
- candidates.push(join17(globalPrefix, "node_modules", "helixevo", "dashboard"));
16523
+ candidates.push(join18(globalPrefix, "lib", "node_modules", "helixevo", "dashboard"));
16524
+ candidates.push(join18(globalPrefix, "node_modules", "helixevo", "dashboard"));
15507
16525
  } catch {}
15508
16526
  for (const dir of candidates) {
15509
- if (existsSync11(join17(dir, "package.json"))) {
16527
+ if (existsSync12(join18(dir, "package.json"))) {
15510
16528
  return dir;
15511
16529
  }
15512
16530
  }
@@ -15514,10 +16532,10 @@ function findNpmDashboard() {
15514
16532
  }
15515
16533
  function getInstalledDashboardVersion() {
15516
16534
  try {
15517
- const versionFile = join17(HELIX_DASHBOARD_DIR, ".helixevo-version");
15518
- if (!existsSync11(versionFile))
16535
+ const versionFile = join18(HELIX_DASHBOARD_DIR, ".helixevo-version");
16536
+ if (!existsSync12(versionFile))
15519
16537
  return null;
15520
- return readFileSync9(versionFile, "utf-8").trim();
16538
+ return readFileSync10(versionFile, "utf-8").trim();
15521
16539
  } catch {
15522
16540
  return null;
15523
16541
  }
@@ -15525,8 +16543,8 @@ function getInstalledDashboardVersion() {
15525
16543
  function copyToHelix(sourceDir) {
15526
16544
  let sourceVersion = VERSION;
15527
16545
  try {
15528
- const srcRoot = join17(sourceDir, "..", "package.json");
15529
- const pkg2 = JSON.parse(readFileSync9(srcRoot, "utf-8"));
16546
+ const srcRoot = join18(sourceDir, "..", "package.json");
16547
+ const pkg2 = JSON.parse(readFileSync10(srcRoot, "utf-8"));
15530
16548
  if (pkg2.name === "helixevo" && pkg2.version)
15531
16549
  sourceVersion = pkg2.version;
15532
16550
  } catch {}
@@ -15542,8 +16560,8 @@ function copyToHelix(sourceDir) {
15542
16560
  mkdirSync5(HELIX_DASHBOARD_DIR, { recursive: true });
15543
16561
  let depsChanged = true;
15544
16562
  try {
15545
- const srcDeps = readFileSync9(join17(sourceDir, "package.json"), "utf-8");
15546
- const dstDeps = readFileSync9(join17(HELIX_DASHBOARD_DIR, "package.json"), "utf-8");
16563
+ const srcDeps = readFileSync10(join18(sourceDir, "package.json"), "utf-8");
16564
+ const dstDeps = readFileSync10(join18(HELIX_DASHBOARD_DIR, "package.json"), "utf-8");
15547
16565
  const srcPkg = JSON.parse(srcDeps);
15548
16566
  const dstPkg = JSON.parse(dstDeps);
15549
16567
  depsChanged = JSON.stringify(srcPkg.dependencies) !== JSON.stringify(dstPkg.dependencies) || JSON.stringify(srcPkg.devDependencies) !== JSON.stringify(dstPkg.devDependencies);
@@ -15554,22 +16572,22 @@ function copyToHelix(sourceDir) {
15554
16572
  for (const item of items) {
15555
16573
  if (item.name === "node_modules" || item.name === ".next" || item.name === "package-lock.json" || item.name === ".env.local")
15556
16574
  continue;
15557
- const src = join17(sourceDir, item.name);
15558
- const dest = join17(HELIX_DASHBOARD_DIR, item.name);
16575
+ const src = join18(sourceDir, item.name);
16576
+ const dest = join18(HELIX_DASHBOARD_DIR, item.name);
15559
16577
  cpSync3(src, dest, { recursive: true });
15560
16578
  }
15561
16579
  if (depsChanged) {
15562
- const oldModules = join17(HELIX_DASHBOARD_DIR, "node_modules");
15563
- if (existsSync11(oldModules))
15564
- rmSync2(oldModules, { recursive: true });
15565
- const oldLock = join17(HELIX_DASHBOARD_DIR, "package-lock.json");
15566
- if (existsSync11(oldLock))
15567
- rmSync2(oldLock);
15568
- }
15569
- const nextCache = join17(HELIX_DASHBOARD_DIR, ".next");
15570
- if (existsSync11(nextCache))
15571
- rmSync2(nextCache, { recursive: true });
15572
- writeFileSync10(join17(HELIX_DASHBOARD_DIR, ".helixevo-version"), sourceVersion);
16580
+ const oldModules = join18(HELIX_DASHBOARD_DIR, "node_modules");
16581
+ if (existsSync12(oldModules))
16582
+ rmSync3(oldModules, { recursive: true });
16583
+ const oldLock = join18(HELIX_DASHBOARD_DIR, "package-lock.json");
16584
+ if (existsSync12(oldLock))
16585
+ rmSync3(oldLock);
16586
+ }
16587
+ const nextCache = join18(HELIX_DASHBOARD_DIR, ".next");
16588
+ if (existsSync12(nextCache))
16589
+ rmSync3(nextCache, { recursive: true });
16590
+ writeFileSync10(join18(HELIX_DASHBOARD_DIR, ".helixevo-version"), sourceVersion);
15573
16591
  return HELIX_DASHBOARD_DIR;
15574
16592
  }
15575
16593
  function ensureSkillGraph() {
@@ -15611,13 +16629,13 @@ function ensureSkillGraph() {
15611
16629
  }
15612
16630
 
15613
16631
  // src/commands/watch.ts
15614
- import { join as join19 } from "node:path";
15615
- import { existsSync as existsSync14 } from "node:fs";
16632
+ import { join as join20 } from "node:path";
16633
+ import { existsSync as existsSync15 } from "node:fs";
15616
16634
 
15617
16635
  // src/core/auto-capture.ts
15618
16636
  init_data();
15619
16637
  init_llm();
15620
- import { readFileSync as readFileSync10, existsSync as existsSync12, statSync as statSync2 } from "node:fs";
16638
+ import { readFileSync as readFileSync11, existsSync as existsSync13, statSync as statSync2 } from "node:fs";
15621
16639
  var CORRECTION_SIGNALS = [
15622
16640
  /\bno[, ]+(?:that's|thats|that is)\s+(?:wrong|incorrect|not right)/i,
15623
16641
  /\bdon'?t\s+do\s+(?:that|it\s+like\s+that)/i,
@@ -15705,17 +16723,17 @@ If no corrections found, return: { "corrections": [] }`
15705
16723
  }
15706
16724
  function watchEvents(options) {
15707
16725
  const { eventsPath, project, onCapture, onError, pollIntervalMs = 5000 } = options;
15708
- let lastSize = existsSync12(eventsPath) ? statSync2(eventsPath).size : 0;
16726
+ let lastSize = existsSync13(eventsPath) ? statSync2(eventsPath).size : 0;
15709
16727
  let messageBuffer = [];
15710
16728
  let pendingExtraction = false;
15711
16729
  const interval = setInterval(async () => {
15712
16730
  try {
15713
- if (!existsSync12(eventsPath))
16731
+ if (!existsSync13(eventsPath))
15714
16732
  return;
15715
16733
  const currentSize = statSync2(eventsPath).size;
15716
16734
  if (currentSize <= lastSize)
15717
16735
  return;
15718
- const content = readFileSync10(eventsPath, "utf-8");
16736
+ const content = readFileSync11(eventsPath, "utf-8");
15719
16737
  const lines = content.trim().split(`
15720
16738
  `).filter(Boolean);
15721
16739
  const newLines = lines.slice(messageBuffer.length);
@@ -15757,17 +16775,17 @@ init_data();
15757
16775
  // src/core/metrics.ts
15758
16776
  init_config();
15759
16777
  init_data();
15760
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync11, existsSync as existsSync13 } from "node:fs";
15761
- import { join as join18 } from "node:path";
16778
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync11, existsSync as existsSync14 } from "node:fs";
16779
+ import { join as join19 } from "node:path";
15762
16780
  function getMetricsPath() {
15763
- return join18(getHelixDir(), "metrics.json");
16781
+ return join19(getHelixDir(), "metrics.json");
15764
16782
  }
15765
16783
  function loadMetrics() {
15766
16784
  const path = getMetricsPath();
15767
- if (!existsSync13(path)) {
16785
+ if (!existsSync14(path)) {
15768
16786
  return { updated: new Date().toISOString(), skills: [], globalCorrectionRate: [], evolutionImpact: [] };
15769
16787
  }
15770
- return JSON.parse(readFileSync11(path, "utf-8"));
16788
+ return JSON.parse(readFileSync12(path, "utf-8"));
15771
16789
  }
15772
16790
  function saveMetrics(store) {
15773
16791
  ensureDir(getHelixDir());
@@ -15947,14 +16965,14 @@ async function watchCommand(options) {
15947
16965
  const verbose = options.verbose ?? false;
15948
16966
  const autoEvolve = options.evolve !== false;
15949
16967
  const project = options.project ?? null;
15950
- const eventsPath = options.events ?? join19(process.cwd(), "events.jsonl");
16968
+ const eventsPath = options.events ?? join20(process.cwd(), "events.jsonl");
15951
16969
  console.log(`\uD83E\uDDEC HelixEvo Watch Mode — Always-On Learning
15952
16970
  `);
15953
16971
  console.log(` Events: ${eventsPath}`);
15954
16972
  console.log(` Project: ${project ?? "(auto-detect)"}`);
15955
16973
  console.log(` Auto-evolve: ${autoEvolve ? "ON" : "OFF"}`);
15956
16974
  console.log();
15957
- if (!existsSync14(eventsPath)) {
16975
+ if (!existsSync15(eventsPath)) {
15958
16976
  console.log(` ⚠ Events file not found yet. Will start watching when it appears.`);
15959
16977
  console.log(` Expected at: ${eventsPath}
15960
16978
  `);
@@ -16107,8 +17125,8 @@ async function metricsCommand(options) {
16107
17125
  init_data();
16108
17126
  init_skills();
16109
17127
  init_llm();
16110
- import { isAbsolute as isAbsolute2, join as join20, normalize, resolve as resolve2 } from "node:path";
16111
- import { existsSync as existsSync15, readFileSync as readFileSync12, writeFileSync as writeFileSync12, mkdirSync as mkdirSync6, readdirSync as readdirSync4 } from "node:fs";
17128
+ import { isAbsolute as isAbsolute2, join as join21, normalize, resolve as resolve2 } from "node:path";
17129
+ import { existsSync as existsSync16, readFileSync as readFileSync13, writeFileSync as writeFileSync12, mkdirSync as mkdirSync6, readdirSync as readdirSync4 } from "node:fs";
16112
17130
  import { homedir as homedir5 } from "node:os";
16113
17131
 
16114
17132
  // src/prompts/project-analysis.ts
@@ -16182,7 +17200,7 @@ function expandHomePath(path) {
16182
17200
  if (path === "~")
16183
17201
  return homedir5();
16184
17202
  if (path.startsWith("~/"))
16185
- return join20(homedir5(), path.slice(2));
17203
+ return join21(homedir5(), path.slice(2));
16186
17204
  return path;
16187
17205
  }
16188
17206
  function normalizeProjectPath(projectPath) {
@@ -16207,10 +17225,10 @@ function readProjectContext(projectPath) {
16207
17225
  "Dockerfile"
16208
17226
  ];
16209
17227
  for (const f of filesToRead) {
16210
- const p = join20(projectPath, f);
16211
- if (existsSync15(p)) {
17228
+ const p = join21(projectPath, f);
17229
+ if (existsSync16(p)) {
16212
17230
  try {
16213
- const content = readFileSync12(p, "utf-8");
17231
+ const content = readFileSync13(p, "utf-8");
16214
17232
  contextFiles.push({ name: f, content: content.slice(0, 2000) });
16215
17233
  } catch {}
16216
17234
  }
@@ -16222,8 +17240,8 @@ function readProjectContext(projectPath) {
16222
17240
  `);
16223
17241
  } catch {}
16224
17242
  let srcStructure = "";
16225
- const srcDir = join20(projectPath, "src");
16226
- if (existsSync15(srcDir)) {
17243
+ const srcDir = join21(projectPath, "src");
17244
+ if (existsSync16(srcDir)) {
16227
17245
  try {
16228
17246
  const scanDir = (dir, prefix, depth) => {
16229
17247
  if (depth > 2)
@@ -16233,7 +17251,7 @@ function readProjectContext(projectPath) {
16233
17251
  for (const item of items) {
16234
17252
  lines.push(`${prefix}${item.isDirectory() ? "\uD83D\uDCC1" : "\uD83D\uDCC4"} ${item.name}`);
16235
17253
  if (item.isDirectory()) {
16236
- lines.push(...scanDir(join20(dir, item.name), prefix + " ", depth + 1));
17254
+ lines.push(...scanDir(join21(dir, item.name), prefix + " ", depth + 1));
16237
17255
  }
16238
17256
  }
16239
17257
  return lines;
@@ -16267,18 +17285,18 @@ ${f.content}
16267
17285
  return context;
16268
17286
  }
16269
17287
  function getProjectsDir() {
16270
- return join20(getHelixDir(), "projects");
17288
+ return join21(getHelixDir(), "projects");
16271
17289
  }
16272
17290
  function saveProjectProfile(profile) {
16273
- const dir = join20(getProjectsDir(), profile.name);
17291
+ const dir = join21(getProjectsDir(), profile.name);
16274
17292
  mkdirSync6(dir, { recursive: true });
16275
- writeFileSync12(join20(dir, "profile.json"), JSON.stringify(profile, null, 2));
17293
+ writeFileSync12(join21(dir, "profile.json"), JSON.stringify(profile, null, 2));
16276
17294
  }
16277
17295
  async function projectSetupCommand(projectPath, options) {
16278
17296
  const verbose = options.verbose ?? false;
16279
17297
  const dryRun = options.dryRun ?? false;
16280
17298
  const resolvedPath = normalizeProjectPath(projectPath);
16281
- if (!existsSync15(resolvedPath)) {
17299
+ if (!existsSync16(resolvedPath)) {
16282
17300
  console.error(` ✗ Path not found: ${resolvedPath}`);
16283
17301
  process.exit(1);
16284
17302
  }
@@ -16408,8 +17426,8 @@ init_data();
16408
17426
  // src/core/topology-apply.ts
16409
17427
  init_config();
16410
17428
  init_data();
16411
- import { copyFileSync, existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync13 } from "node:fs";
16412
- import { join as join21 } from "node:path";
17429
+ import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync7, readFileSync as readFileSync14, writeFileSync as writeFileSync13 } from "node:fs";
17430
+ import { join as join22 } from "node:path";
16413
17431
  function uniqueStrings2(values) {
16414
17432
  return [...new Set(values.filter((value) => Boolean(value && value.trim())))];
16415
17433
  }
@@ -16435,24 +17453,24 @@ function snapshotId(prefix) {
16435
17453
  return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`.replace(/[^a-zA-Z0-9_-]/g, "-");
16436
17454
  }
16437
17455
  function readSkillRaw(slug) {
16438
- const path = join21(getGeneralSkillsPath(), slug, "SKILL.md");
16439
- if (!existsSync16(path))
17456
+ const path = join22(getGeneralSkillsPath(), slug, "SKILL.md");
17457
+ if (!existsSync17(path))
16440
17458
  return null;
16441
- return { path, raw: readFileSync13(path, "utf-8") };
17459
+ return { path, raw: readFileSync14(path, "utf-8") };
16442
17460
  }
16443
17461
  function createSnapshot(params) {
16444
17462
  const id = snapshotId("topology_snapshot");
16445
- const snapshotDir = join21(getHelixDir(), "topology-snapshots", id);
17463
+ const snapshotDir = join22(getHelixDir(), "topology-snapshots", id);
16446
17464
  mkdirSync7(snapshotDir, { recursive: true });
16447
- const graphPath = join21(snapshotDir, "skill-graph.json");
16448
- const overridesPath = join21(snapshotDir, "topology-overrides.json");
17465
+ const graphPath = join22(snapshotDir, "skill-graph.json");
17466
+ const overridesPath = join22(snapshotDir, "topology-overrides.json");
16449
17467
  writeFileSync13(graphPath, JSON.stringify(loadSkillGraph(), null, 2));
16450
17468
  writeFileSync13(overridesPath, JSON.stringify(loadTopologyOverrides(), null, 2));
16451
17469
  const skillFileSnapshots = uniqueStrings2(params.skillSlugs ?? []).flatMap((slug) => {
16452
17470
  const skill = readSkillRaw(slug);
16453
17471
  if (!skill)
16454
17472
  return [];
16455
- const snapshotPath = join21(snapshotDir, `${slug}.SKILL.md`);
17473
+ const snapshotPath = join22(snapshotDir, `${slug}.SKILL.md`);
16456
17474
  copyFileSync(skill.path, snapshotPath);
16457
17475
  return [{ slug, path: skill.path, snapshotPath }];
16458
17476
  });
@@ -16693,10 +17711,10 @@ function rollbackAppliedTopologyPlan(planId) {
16693
17711
  throw new Error(`Topology plan ${planId} must be applied before rollback`);
16694
17712
  }
16695
17713
  const beforeSnapshot = loadTopologySnapshots().find((snapshot) => snapshot.id === plan.beforeSnapshotId);
16696
- if (!beforeSnapshot || !existsSync16(beforeSnapshot.overridesPath)) {
17714
+ if (!beforeSnapshot || !existsSync17(beforeSnapshot.overridesPath)) {
16697
17715
  throw new Error(`Before-apply snapshot for ${planId} is missing or incomplete`);
16698
17716
  }
16699
- const restoredOverrides = JSON.parse(readFileSync13(beforeSnapshot.overridesPath, "utf-8"));
17717
+ const restoredOverrides = JSON.parse(readFileSync14(beforeSnapshot.overridesPath, "utf-8"));
16700
17718
  saveTopologyOverrides({
16701
17719
  ...restoredOverrides,
16702
17720
  updatedAt: new Date().toISOString()
@@ -16972,14 +17990,14 @@ async function ontologyCommand(options) {
16972
17990
  }
16973
17991
 
16974
17992
  // src/core/proof.ts
16975
- import { existsSync as existsSync17, readFileSync as readFileSync14 } from "node:fs";
16976
- import { join as join22 } from "node:path";
17993
+ import { existsSync as existsSync18, readFileSync as readFileSync15 } from "node:fs";
17994
+ import { join as join23 } from "node:path";
16977
17995
  init_config();
16978
17996
  init_data();
16979
17997
  var PROOF_REVIEW_FILE = "proof-reviews.jsonl";
16980
17998
  var MEASURING_WINDOW_DAYS = 2;
16981
17999
  function proofPath() {
16982
- return join22(getHelixDir(), PROOF_REVIEW_FILE);
18000
+ return join23(getHelixDir(), PROOF_REVIEW_FILE);
16983
18001
  }
16984
18002
  function uniqueStrings3(values) {
16985
18003
  return [...new Set(values.filter((value) => Boolean(value && value.trim())))];
@@ -16998,9 +18016,9 @@ function buildRecordId(targetType, targetId) {
16998
18016
  }
16999
18017
  function readProofReviews() {
17000
18018
  const path = proofPath();
17001
- if (!existsSync17(path))
18019
+ if (!existsSync18(path))
17002
18020
  return [];
17003
- const raw = readFileSync14(path, "utf-8").trim();
18021
+ const raw = readFileSync15(path, "utf-8").trim();
17004
18022
  if (!raw)
17005
18023
  return [];
17006
18024
  return raw.split(`
@@ -17476,7 +18494,7 @@ Examples:
17476
18494
  $ helixevo graph --mermaid Open graph in browser
17477
18495
  $ helixevo graph --obsidian ~/vault Sync to Obsidian vault
17478
18496
  $ helixevo graph --rebuild Re-infer relationships (LLM call)
17479
- $ helixevo graph --optimize Detect structural candidates + refresh topology review queue
18497
+ $ helixevo graph --optimize Refresh topology review queue first, then report full vs partial enrichment
17480
18498
  $ helixevo topology --status Show reviewed topology execution state
17481
18499
  $ helixevo topology --prepare <id> Prepare an accepted topology candidate
17482
18500
  $ helixevo topology --apply <planId> Apply a safe prepared topology plan
@@ -17495,12 +18513,12 @@ program2.command("capture").description("Capture failures from a Craft Agent ses
17495
18513
  program2.command("evolve").description("Evolve skills from failures [--dry-run] [--verbose] [--max-proposals <n>]").option("--dry-run", "Show proposals without applying").option("--verbose", "Show detailed LLM interactions").option("--max-proposals <n>", "Max proposals per run", "5").action(evolveCommand);
17496
18514
  program2.command("generalize").description("Promote cross-skill patterns to higher layer ↑ [--dry-run] [--verbose]").option("--dry-run", "Show candidates without applying").option("--verbose", "Show detailed analysis").action(generalizeCommand);
17497
18515
  program2.command("specialize").description("Create project-specific skills ↓ --project <name> [--dry-run] [--verbose]").requiredOption("--project <name>", "Project name").option("--dry-run", "Show candidates without applying").option("--verbose", "Show detailed analysis").action(specializeCommand);
17498
- program2.command("graph").description("Skill network [--mermaid] [--obsidian <path>] [--rebuild] [--optimize refreshes topology review]").option("--mermaid", "Render as Mermaid diagram in browser").option("--rebuild", "Force rebuild (re-infer relationships via LLM)").option("--obsidian <path>", "Sync to Obsidian vault at path").option("--optimize", "Run network optimization (merge/split/conflict detection)").option("--verbose", "Show detailed analysis").action(graphCommand);
18516
+ program2.command("graph").description("Skill network [--mermaid] [--obsidian <path>] [--rebuild] [--optimize refreshes topology review and reports full vs partial enrichment]").option("--mermaid", "Render as Mermaid diagram in browser").option("--rebuild", "Force rebuild (re-infer relationships via LLM)").option("--obsidian <path>", "Sync to Obsidian vault at path").option("--optimize", "Run network optimization (merge/split/conflict detection)").option("--verbose", "Show detailed analysis").action(graphCommand);
17499
18517
  program2.command("research").description("Proactive research via web [--project <path>] [--dry-run] [--verbose]").option("--project <path>", "Project path for goal extraction").option("--dry-run", "Show discoveries without creating skills").option("--verbose", "Show detailed research steps").option("--max-hypotheses <n>", "Max hypotheses to test", "3").action(researchCommand);
17500
18518
  program2.command("dashboard").description("Open web dashboard (default: http://localhost:3847, falls forward if occupied)").option("--background", "Run in background (detach from terminal)").option("--stop", "Stop a background dashboard").option("--port <port>", "Preferred port to try first (default: 3847)").option("--no-auto-update", "Skip automatic update check before launching the dashboard").action(async (options) => {
17501
18519
  await dashboardCommand(options);
17502
18520
  });
17503
- program2.command("status").description("Show frontier, skills, failures, and network health").action(statusCommand);
18521
+ program2.command("status").description("Show frontier, skills, failures, network health, and provider control truth").action(statusCommand);
17504
18522
  program2.command("report").description("Evolution report [--days <n>] [--output <path>]").option("--days <n>", "Report period in days", "1").option("--output <path>", "Output path for report").action(reportCommand);
17505
18523
  program2.command("watch").description("Always-on learning: auto-capture corrections + auto-evolve").option("--project <name>", "Project name for captured failures").option("--events <path>", "Path to events.jsonl (default: ./events.jsonl)").option("--verbose", "Show detailed capture and metrics").option("--no-evolve", "Disable auto-evolution (capture only)").action(watchCommand);
17506
18524
  program2.command("health").description("Assess network health: cohesion, coverage, balance, cross-project transfer").option("--verbose", "Show detailed analysis").action(async (options) => {
@@ -17516,4 +18534,10 @@ program2.command("ontology").description("Governed ontology frontier and semanti
17516
18534
  program2.hook("postAction", () => {
17517
18535
  checkForUpdates().catch(() => {});
17518
18536
  });
17519
- program2.parse();
18537
+ try {
18538
+ await program2.parseAsync(process.argv);
18539
+ } catch (error) {
18540
+ const message = error instanceof Error ? error.message : String(error);
18541
+ console.error(message);
18542
+ process.exit(1);
18543
+ }