@covibes/zeroshot 5.2.1 → 5.4.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/CHANGELOG.md +174 -189
- package/README.md +226 -195
- package/cli/commands/providers.js +149 -0
- package/cli/index.js +3145 -2366
- package/cli/lib/first-run.js +40 -3
- package/cli/message-formatters-normal.js +28 -6
- package/cluster-templates/base-templates/debug-workflow.json +24 -78
- package/cluster-templates/base-templates/full-workflow.json +99 -316
- package/cluster-templates/base-templates/single-worker.json +23 -15
- package/cluster-templates/base-templates/worker-validator.json +105 -36
- package/cluster-templates/conductor-bootstrap.json +9 -7
- package/lib/docker-config.js +14 -1
- package/lib/git-remote-utils.js +165 -0
- package/lib/id-detector.js +10 -7
- package/lib/provider-defaults.js +62 -0
- package/lib/provider-detection.js +59 -0
- package/lib/provider-names.js +57 -0
- package/lib/settings/claude-auth.js +78 -0
- package/lib/settings.js +298 -15
- package/lib/stream-json-parser.js +4 -238
- package/package.json +27 -6
- package/scripts/setup-merge-queue.sh +170 -0
- package/scripts/validate-templates.js +100 -0
- package/src/agent/agent-config.js +140 -63
- package/src/agent/agent-context-builder.js +336 -165
- package/src/agent/agent-hook-executor.js +337 -67
- package/src/agent/agent-lifecycle.js +386 -287
- package/src/agent/agent-stuck-detector.js +7 -7
- package/src/agent/agent-task-executor.js +944 -683
- package/src/agent/output-extraction.js +217 -0
- package/src/agent/output-reformatter.js +175 -0
- package/src/agent/schema-utils.js +146 -0
- package/src/agent-wrapper.js +112 -31
- package/src/agents/git-pusher-template.js +285 -0
- package/src/claude-task-runner.js +145 -44
- package/src/config-router.js +13 -13
- package/src/config-validator.js +1049 -563
- package/src/input-helpers.js +65 -0
- package/src/isolation-manager.js +499 -320
- package/src/issue-providers/README.md +305 -0
- package/src/issue-providers/azure-devops-provider.js +273 -0
- package/src/issue-providers/base-provider.js +232 -0
- package/src/issue-providers/github-provider.js +179 -0
- package/src/issue-providers/gitlab-provider.js +241 -0
- package/src/issue-providers/index.js +196 -0
- package/src/issue-providers/jira-provider.js +239 -0
- package/src/ledger.js +50 -11
- package/src/lib/safe-exec.js +88 -0
- package/src/orchestrator.js +1348 -757
- package/src/preflight.js +306 -149
- package/src/process-metrics.js +98 -56
- package/src/providers/anthropic/cli-builder.js +73 -0
- package/src/providers/anthropic/index.js +204 -0
- package/src/providers/anthropic/models.js +23 -0
- package/src/providers/anthropic/output-parser.js +177 -0
- package/src/providers/base-provider.js +251 -0
- package/src/providers/capabilities.js +60 -0
- package/src/providers/google/cli-builder.js +55 -0
- package/src/providers/google/index.js +116 -0
- package/src/providers/google/models.js +24 -0
- package/src/providers/google/output-parser.js +101 -0
- package/src/providers/index.js +91 -0
- package/src/providers/openai/cli-builder.js +133 -0
- package/src/providers/openai/index.js +136 -0
- package/src/providers/openai/models.js +21 -0
- package/src/providers/openai/output-parser.js +143 -0
- package/src/providers/opencode/cli-builder.js +42 -0
- package/src/providers/opencode/index.js +103 -0
- package/src/providers/opencode/models.js +55 -0
- package/src/providers/opencode/output-parser.js +122 -0
- package/src/schemas/sub-cluster.js +68 -39
- package/src/status-footer.js +94 -48
- package/src/sub-cluster-wrapper.js +92 -36
- package/src/task-runner.js +8 -6
- package/src/template-resolver.js +12 -9
- package/src/tui/data-poller.js +123 -99
- package/src/tui/formatters.js +4 -3
- package/src/tui/keybindings.js +259 -318
- package/src/tui/layout.js +20 -3
- package/src/tui/renderer.js +11 -21
- package/task-lib/attachable-watcher.js +150 -111
- package/task-lib/claude-recovery.js +156 -0
- package/task-lib/commands/episodes.js +105 -0
- package/task-lib/commands/list.js +3 -3
- package/task-lib/commands/logs.js +231 -189
- package/task-lib/commands/resume.js +3 -2
- package/task-lib/commands/run.js +12 -3
- package/task-lib/commands/schedules.js +111 -61
- package/task-lib/config.js +0 -2
- package/task-lib/runner.js +128 -50
- package/task-lib/scheduler.js +3 -3
- package/task-lib/store.js +464 -152
- package/task-lib/tui/formatters.js +94 -45
- package/task-lib/tui/renderer.js +13 -3
- package/task-lib/tui.js +73 -32
- package/task-lib/watcher.js +157 -100
- package/src/agents/git-pusher-agent.json +0 -20
- package/src/github.js +0 -103
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider defaults module
|
|
3
|
+
*
|
|
4
|
+
* Separated from settings.js to break circular dependency:
|
|
5
|
+
* - settings.js requires provider defaults
|
|
6
|
+
* - providers require settings.js (for loadSettings, getClaudeCommand, etc.)
|
|
7
|
+
*
|
|
8
|
+
* CRITICAL: This module should NOT import from settings.js
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Cache provider defaults to avoid repeated instantiation
|
|
12
|
+
let _providerDefaultsCache = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build provider default settings by instantiating each provider
|
|
16
|
+
* and calling getDefaultSettings()
|
|
17
|
+
* @returns {Object} Map of provider name to default settings
|
|
18
|
+
*/
|
|
19
|
+
function buildProviderDefaults() {
|
|
20
|
+
const { listProviders, getProvider } = require('../src/providers');
|
|
21
|
+
|
|
22
|
+
const defaults = {};
|
|
23
|
+
for (const providerName of listProviders()) {
|
|
24
|
+
try {
|
|
25
|
+
const provider = getProvider(providerName);
|
|
26
|
+
defaults[providerName] = provider.getDefaultSettings();
|
|
27
|
+
} catch (error) {
|
|
28
|
+
// If provider fails to instantiate, use basic defaults
|
|
29
|
+
console.warn(`Warning: Could not get defaults for ${providerName}: ${error.message}`);
|
|
30
|
+
defaults[providerName] = {
|
|
31
|
+
maxLevel: 'level3',
|
|
32
|
+
minLevel: 'level1',
|
|
33
|
+
defaultLevel: 'level2',
|
|
34
|
+
levelOverrides: {},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return defaults;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get or build cached provider defaults
|
|
43
|
+
* @returns {Object} Provider defaults
|
|
44
|
+
*/
|
|
45
|
+
function getProviderDefaults() {
|
|
46
|
+
if (!_providerDefaultsCache) {
|
|
47
|
+
_providerDefaultsCache = buildProviderDefaults();
|
|
48
|
+
}
|
|
49
|
+
return _providerDefaultsCache;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Clear the provider defaults cache (primarily for testing)
|
|
54
|
+
*/
|
|
55
|
+
function clearProviderDefaultsCache() {
|
|
56
|
+
_providerDefaultsCache = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
getProviderDefaults,
|
|
61
|
+
clearProviderDefaultsCache,
|
|
62
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const { execSync, spawnSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
function commandExists(command) {
|
|
6
|
+
if (!command) return false;
|
|
7
|
+
if (command.includes(path.sep)) {
|
|
8
|
+
return fs.existsSync(command);
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
execSync(`command -v ${command}`, { stdio: 'pipe' });
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getCommandPath(command) {
|
|
19
|
+
if (!command) return null;
|
|
20
|
+
if (command.includes(path.sep)) {
|
|
21
|
+
return fs.existsSync(command) ? command : null;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const output = execSync(`command -v ${command}`, { encoding: 'utf8', stdio: 'pipe' });
|
|
25
|
+
return output.trim() || null;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getHelpOutput(command, args = []) {
|
|
32
|
+
if (!commandExists(command)) return '';
|
|
33
|
+
|
|
34
|
+
const attempt = (flag) => {
|
|
35
|
+
const result = spawnSync(command, [...args, flag], { encoding: 'utf8' });
|
|
36
|
+
const output = `${result.stdout || ''}${result.stderr || ''}`;
|
|
37
|
+
return output.trim();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const help = attempt('--help');
|
|
41
|
+
if (help) return help;
|
|
42
|
+
|
|
43
|
+
const alt = attempt('-h');
|
|
44
|
+
return alt || '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getVersionOutput(command, args = []) {
|
|
48
|
+
if (!commandExists(command)) return '';
|
|
49
|
+
const result = spawnSync(command, [...args, '--version'], { encoding: 'utf8' });
|
|
50
|
+
const output = `${result.stdout || ''}${result.stderr || ''}`;
|
|
51
|
+
return output.trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
commandExists,
|
|
56
|
+
getCommandPath,
|
|
57
|
+
getHelpOutput,
|
|
58
|
+
getVersionOutput,
|
|
59
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const PROVIDER_ALIASES = {
|
|
2
|
+
anthropic: 'claude',
|
|
3
|
+
openai: 'codex',
|
|
4
|
+
google: 'gemini',
|
|
5
|
+
claude: 'claude',
|
|
6
|
+
codex: 'codex',
|
|
7
|
+
gemini: 'gemini',
|
|
8
|
+
opencode: 'opencode',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const VALID_PROVIDERS = ['claude', 'codex', 'gemini', 'opencode'];
|
|
12
|
+
|
|
13
|
+
function normalizeProviderName(name) {
|
|
14
|
+
if (!name || typeof name !== 'string') return name;
|
|
15
|
+
const normalized = PROVIDER_ALIASES[name.toLowerCase()];
|
|
16
|
+
return normalized || name;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeProviderSettings(providerSettings) {
|
|
20
|
+
if (
|
|
21
|
+
!providerSettings ||
|
|
22
|
+
typeof providerSettings !== 'object' ||
|
|
23
|
+
Array.isArray(providerSettings)
|
|
24
|
+
) {
|
|
25
|
+
return providerSettings;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const normalized = {};
|
|
29
|
+
const entries = Object.entries(providerSettings);
|
|
30
|
+
const aliasFirst = entries.sort(([left], [right]) => {
|
|
31
|
+
const leftIsCanonical = normalizeProviderName(left) === left;
|
|
32
|
+
const rightIsCanonical = normalizeProviderName(right) === right;
|
|
33
|
+
if (leftIsCanonical === rightIsCanonical) return 0;
|
|
34
|
+
return leftIsCanonical ? 1 : -1;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
for (const [key, value] of aliasFirst) {
|
|
38
|
+
const canonical = normalizeProviderName(key);
|
|
39
|
+
if (!VALID_PROVIDERS.includes(canonical)) {
|
|
40
|
+
normalized[key] = value;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
normalized[canonical] = {
|
|
44
|
+
...(normalized[canonical] || {}),
|
|
45
|
+
...(value || {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
PROVIDER_ALIASES,
|
|
54
|
+
VALID_PROVIDERS,
|
|
55
|
+
normalizeProviderName,
|
|
56
|
+
normalizeProviderSettings,
|
|
57
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude authentication settings and helpers
|
|
3
|
+
* Extracted from settings.js for provider-specific isolation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Anthropic API key prefix for validation
|
|
8
|
+
*/
|
|
9
|
+
const ANTHROPIC_KEY_PREFIX = 'sk-ant-';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Environment variables used for Claude authentication
|
|
13
|
+
*/
|
|
14
|
+
const CLAUDE_AUTH_ENV_VARS = [
|
|
15
|
+
'ANTHROPIC_API_KEY',
|
|
16
|
+
'AWS_BEARER_TOKEN_BEDROCK',
|
|
17
|
+
'AWS_REGION',
|
|
18
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate an Anthropic API key format
|
|
23
|
+
* @param {string|null|undefined} key - API key to validate
|
|
24
|
+
* @returns {boolean} True if key is falsy OR starts with valid prefix
|
|
25
|
+
*/
|
|
26
|
+
function isValidAnthropicKey(key) {
|
|
27
|
+
return !key || key.startsWith(ANTHROPIC_KEY_PREFIX);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if Bedrock mode is active based on env and overrides
|
|
32
|
+
* @param {Object} [envOverrides={}] - Environment variable overrides to check
|
|
33
|
+
* @returns {boolean} True if Bedrock mode is active
|
|
34
|
+
*/
|
|
35
|
+
function isBedrockMode(envOverrides = {}) {
|
|
36
|
+
return (
|
|
37
|
+
envOverrides.CLAUDE_CODE_USE_BEDROCK === '1' || process.env.CLAUDE_CODE_USE_BEDROCK === '1'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve Claude authentication environment variables from settings
|
|
43
|
+
* Bedrock takes priority over direct Anthropic API key AND OAuth session
|
|
44
|
+
* @param {Object} settings - Settings object from loadSettings()
|
|
45
|
+
* @returns {Object} Environment variables to set for Claude auth
|
|
46
|
+
*/
|
|
47
|
+
function resolveClaudeAuth(settings) {
|
|
48
|
+
const claudeSettings = settings.providerSettings?.claude || {};
|
|
49
|
+
const env = {};
|
|
50
|
+
|
|
51
|
+
// Bedrock takes priority over everything (including OAuth)
|
|
52
|
+
if (!process.env.AWS_BEARER_TOKEN_BEDROCK && claudeSettings.bedrockApiKey) {
|
|
53
|
+
env.AWS_BEARER_TOKEN_BEDROCK = claudeSettings.bedrockApiKey;
|
|
54
|
+
env.CLAUDE_CODE_USE_BEDROCK = '1';
|
|
55
|
+
if (claudeSettings.bedrockRegion && !process.env.AWS_REGION) {
|
|
56
|
+
env.AWS_REGION = claudeSettings.bedrockRegion;
|
|
57
|
+
}
|
|
58
|
+
} else if (process.env.AWS_BEARER_TOKEN_BEDROCK && !process.env.CLAUDE_CODE_USE_BEDROCK) {
|
|
59
|
+
// Auto-set CLAUDE_CODE_USE_BEDROCK if token present in env
|
|
60
|
+
env.CLAUDE_CODE_USE_BEDROCK = '1';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Anthropic API key only if no Bedrock
|
|
64
|
+
const hasBedrock = env.AWS_BEARER_TOKEN_BEDROCK || process.env.AWS_BEARER_TOKEN_BEDROCK;
|
|
65
|
+
if (!process.env.ANTHROPIC_API_KEY && !hasBedrock && claudeSettings.anthropicApiKey) {
|
|
66
|
+
env.ANTHROPIC_API_KEY = claudeSettings.anthropicApiKey;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return env;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
ANTHROPIC_KEY_PREFIX,
|
|
74
|
+
CLAUDE_AUTH_ENV_VARS,
|
|
75
|
+
isValidAnthropicKey,
|
|
76
|
+
isBedrockMode,
|
|
77
|
+
resolveClaudeAuth,
|
|
78
|
+
};
|
package/lib/settings.js
CHANGED
|
@@ -7,6 +7,20 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
const { validateMountConfig, validateEnvPassthrough } = require('./docker-config');
|
|
10
|
+
const {
|
|
11
|
+
VALID_PROVIDERS,
|
|
12
|
+
normalizeProviderName,
|
|
13
|
+
normalizeProviderSettings,
|
|
14
|
+
} = require('./provider-names');
|
|
15
|
+
|
|
16
|
+
// Lazy-loaded to avoid circular dependency (issue-providers → settings → issue-providers)
|
|
17
|
+
let _issueProviderFns = null;
|
|
18
|
+
function getIssueProviderFns() {
|
|
19
|
+
if (!_issueProviderFns) {
|
|
20
|
+
_issueProviderFns = require('../src/issue-providers');
|
|
21
|
+
}
|
|
22
|
+
return _issueProviderFns;
|
|
23
|
+
}
|
|
10
24
|
|
|
11
25
|
/**
|
|
12
26
|
* Get settings file path (dynamically reads env var for testing)
|
|
@@ -14,9 +28,14 @@ const { validateMountConfig, validateEnvPassthrough } = require('./docker-config
|
|
|
14
28
|
* @returns {string}
|
|
15
29
|
*/
|
|
16
30
|
function getSettingsFile() {
|
|
17
|
-
return
|
|
31
|
+
return (
|
|
32
|
+
process.env.ZEROSHOT_SETTINGS_FILE || path.join(os.homedir(), '.zeroshot', 'settings.json')
|
|
33
|
+
);
|
|
18
34
|
}
|
|
19
35
|
|
|
36
|
+
// Import provider defaults from separate module to avoid circular dependency
|
|
37
|
+
const { getProviderDefaults, clearProviderDefaultsCache } = require('./provider-defaults');
|
|
38
|
+
|
|
20
39
|
/**
|
|
21
40
|
* Model hierarchy for cost ceiling validation
|
|
22
41
|
* Higher number = more expensive/capable model
|
|
@@ -28,15 +47,17 @@ const MODEL_HIERARCHY = {
|
|
|
28
47
|
};
|
|
29
48
|
|
|
30
49
|
const VALID_MODELS = Object.keys(MODEL_HIERARCHY);
|
|
50
|
+
const LEVEL_RANKS = { level1: 1, level2: 2, level3: 3 };
|
|
31
51
|
|
|
32
52
|
/**
|
|
33
|
-
* Validate a requested model against the maxModel ceiling
|
|
53
|
+
* Validate a requested model against the maxModel ceiling and minModel floor
|
|
34
54
|
* @param {string} requestedModel - Model the agent wants to use
|
|
35
55
|
* @param {string} maxModel - Maximum allowed model (cost ceiling)
|
|
56
|
+
* @param {string|null} minModel - Minimum required model (cost floor)
|
|
36
57
|
* @returns {string} The validated model
|
|
37
|
-
* @throws {Error} If requested model exceeds ceiling
|
|
58
|
+
* @throws {Error} If requested model exceeds ceiling or falls below floor
|
|
38
59
|
*/
|
|
39
|
-
function validateModelAgainstMax(requestedModel, maxModel) {
|
|
60
|
+
function validateModelAgainstMax(requestedModel, maxModel, minModel = null) {
|
|
40
61
|
if (!requestedModel) return maxModel; // Default to ceiling if unspecified
|
|
41
62
|
|
|
42
63
|
if (!VALID_MODELS.includes(requestedModel)) {
|
|
@@ -52,12 +73,34 @@ function validateModelAgainstMax(requestedModel, maxModel) {
|
|
|
52
73
|
`Either lower agent's model or raise maxModel.`
|
|
53
74
|
);
|
|
54
75
|
}
|
|
76
|
+
|
|
77
|
+
if (minModel) {
|
|
78
|
+
if (!VALID_MODELS.includes(minModel)) {
|
|
79
|
+
throw new Error(`Invalid minModel "${minModel}". Valid: ${VALID_MODELS.join(', ')}`);
|
|
80
|
+
}
|
|
81
|
+
if (MODEL_HIERARCHY[minModel] > MODEL_HIERARCHY[maxModel]) {
|
|
82
|
+
throw new Error(`minModel "${minModel}" cannot be higher than maxModel "${maxModel}".`);
|
|
83
|
+
}
|
|
84
|
+
if (MODEL_HIERARCHY[requestedModel] < MODEL_HIERARCHY[minModel]) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Agent requests "${requestedModel}" but minModel is "${minModel}". ` +
|
|
87
|
+
`Either raise agent's model or lower minModel.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
55
92
|
return requestedModel;
|
|
56
93
|
}
|
|
57
94
|
|
|
58
|
-
// Default settings
|
|
59
|
-
const
|
|
95
|
+
// Default settings (base - without issue provider settings which are added dynamically)
|
|
96
|
+
const DEFAULT_SETTINGS_BASE = {
|
|
60
97
|
maxModel: 'opus', // Cost ceiling - agents cannot use models above this
|
|
98
|
+
minModel: null, // Cost floor - agents cannot use models below this (null = no minimum)
|
|
99
|
+
defaultProvider: 'claude',
|
|
100
|
+
get providerSettings() {
|
|
101
|
+
// Dynamically build from providers on first access
|
|
102
|
+
return getProviderDefaults();
|
|
103
|
+
},
|
|
61
104
|
defaultConfig: 'conductor-bootstrap',
|
|
62
105
|
defaultDocker: false,
|
|
63
106
|
strictSchema: true, // true = reliable json output (default), false = live streaming (may crash - see bold-meadow-11)
|
|
@@ -70,7 +113,7 @@ const DEFAULT_SETTINGS = {
|
|
|
70
113
|
// Example: 'ccr code' for claude-code-router integration
|
|
71
114
|
claudeCommand: 'claude',
|
|
72
115
|
// Docker isolation mounts - preset names or {host, container, readonly?} objects
|
|
73
|
-
// Valid presets: gh, git, ssh, aws, azure, kube, terraform, gcloud
|
|
116
|
+
// Valid presets: gh, git, ssh, aws, azure, kube, terraform, gcloud, claude, codex, gemini, opencode
|
|
74
117
|
dockerMounts: ['gh', 'git', 'ssh'],
|
|
75
118
|
// Extra env vars to pass to Docker container (in addition to preset-implied ones)
|
|
76
119
|
// Supports: VAR (if set), VAR_* (pattern), VAR=value (forced), VAR= (empty)
|
|
@@ -78,22 +121,156 @@ const DEFAULT_SETTINGS = {
|
|
|
78
121
|
// Container home directory - where $HOME resolves in container paths
|
|
79
122
|
// Default: /home/node (matches zeroshot-cluster-base image)
|
|
80
123
|
dockerContainerHome: '/home/node',
|
|
124
|
+
// Issue provider settings - defaultIssueSource is here, others come from providers
|
|
125
|
+
defaultIssueSource: 'github', // 'github' | 'gitlab' | 'jira' | 'azure-devops'
|
|
81
126
|
};
|
|
82
127
|
|
|
128
|
+
// Cache for merged defaults (base + issue provider settings)
|
|
129
|
+
let _defaultSettingsCache = null;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get DEFAULT_SETTINGS with issue provider settings merged in
|
|
133
|
+
* Lazy-loaded to avoid circular dependency at module load time
|
|
134
|
+
*/
|
|
135
|
+
function getDefaultSettings() {
|
|
136
|
+
if (!_defaultSettingsCache) {
|
|
137
|
+
const issueProviderDefaults = getIssueProviderFns().getIssueProviderSettingsDefaults();
|
|
138
|
+
_defaultSettingsCache = {
|
|
139
|
+
...DEFAULT_SETTINGS_BASE,
|
|
140
|
+
...issueProviderDefaults,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return _defaultSettingsCache;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// For backward compatibility, export DEFAULT_SETTINGS as a getter
|
|
147
|
+
const DEFAULT_SETTINGS = new Proxy(DEFAULT_SETTINGS_BASE, {
|
|
148
|
+
get(target, prop) {
|
|
149
|
+
// For known base properties, return from base
|
|
150
|
+
if (prop in target) {
|
|
151
|
+
return target[prop];
|
|
152
|
+
}
|
|
153
|
+
// For issue provider settings, get from merged defaults
|
|
154
|
+
const merged = getDefaultSettings();
|
|
155
|
+
return merged[prop];
|
|
156
|
+
},
|
|
157
|
+
has(target, prop) {
|
|
158
|
+
if (prop in target) return true;
|
|
159
|
+
const merged = getDefaultSettings();
|
|
160
|
+
return prop in merged;
|
|
161
|
+
},
|
|
162
|
+
ownKeys() {
|
|
163
|
+
const merged = getDefaultSettings();
|
|
164
|
+
return Object.keys(merged);
|
|
165
|
+
},
|
|
166
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
167
|
+
const merged = getDefaultSettings();
|
|
168
|
+
if (prop in merged) {
|
|
169
|
+
return { enumerable: true, configurable: true, value: merged[prop] };
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
function mapLegacyModelToLevel(model) {
|
|
176
|
+
switch (model) {
|
|
177
|
+
case 'haiku':
|
|
178
|
+
return 'level1';
|
|
179
|
+
case 'sonnet':
|
|
180
|
+
return 'level2';
|
|
181
|
+
case 'opus':
|
|
182
|
+
return 'level3';
|
|
183
|
+
default:
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function mergeProviderSettings(current, overrides) {
|
|
189
|
+
// Ensure current has all providers with their defaults
|
|
190
|
+
const providerDefaults = getProviderDefaults();
|
|
191
|
+
const merged = {};
|
|
192
|
+
|
|
193
|
+
for (const provider of VALID_PROVIDERS) {
|
|
194
|
+
merged[provider] = {
|
|
195
|
+
...(providerDefaults[provider] || {}),
|
|
196
|
+
...(current[provider] || {}),
|
|
197
|
+
...(overrides?.[provider] || {}),
|
|
198
|
+
};
|
|
199
|
+
if (!merged[provider].levelOverrides) {
|
|
200
|
+
merged[provider].levelOverrides = {};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return merged;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function applyLegacyModelBounds(settings) {
|
|
207
|
+
if (!settings.providerSettings) return settings;
|
|
208
|
+
const claude = settings.providerSettings.claude || {};
|
|
209
|
+
const legacyMaxLevel = mapLegacyModelToLevel(settings.maxModel);
|
|
210
|
+
const legacyMinLevel = mapLegacyModelToLevel(settings.minModel);
|
|
211
|
+
|
|
212
|
+
if (legacyMaxLevel) {
|
|
213
|
+
claude.maxLevel = legacyMaxLevel;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (legacyMinLevel) {
|
|
217
|
+
claude.minLevel = legacyMinLevel;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const minRank = LEVEL_RANKS[claude.minLevel] || LEVEL_RANKS.level1;
|
|
221
|
+
const maxRank = LEVEL_RANKS[claude.maxLevel] || LEVEL_RANKS.level3;
|
|
222
|
+
const defaultRank = LEVEL_RANKS[claude.defaultLevel] || LEVEL_RANKS.level2;
|
|
223
|
+
|
|
224
|
+
if (minRank > maxRank) {
|
|
225
|
+
claude.minLevel = 'level1';
|
|
226
|
+
claude.maxLevel = 'level3';
|
|
227
|
+
} else if (defaultRank < minRank) {
|
|
228
|
+
claude.defaultLevel = claude.minLevel;
|
|
229
|
+
} else if (defaultRank > maxRank) {
|
|
230
|
+
claude.defaultLevel = claude.maxLevel;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
settings.providerSettings.claude = claude;
|
|
234
|
+
return settings;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizeLoadedSettings(parsed) {
|
|
238
|
+
const normalized = { ...parsed };
|
|
239
|
+
if (parsed.defaultProvider) {
|
|
240
|
+
normalized.defaultProvider = normalizeProviderName(parsed.defaultProvider);
|
|
241
|
+
}
|
|
242
|
+
if (parsed.providerSettings) {
|
|
243
|
+
normalized.providerSettings = normalizeProviderSettings(parsed.providerSettings);
|
|
244
|
+
}
|
|
245
|
+
return normalized;
|
|
246
|
+
}
|
|
247
|
+
|
|
83
248
|
/**
|
|
84
249
|
* Load settings from disk, merging with defaults
|
|
85
250
|
*/
|
|
86
251
|
function loadSettings() {
|
|
87
252
|
const settingsFile = getSettingsFile();
|
|
88
253
|
if (!fs.existsSync(settingsFile)) {
|
|
89
|
-
|
|
254
|
+
// Return a copy with resolved providerSettings
|
|
255
|
+
return {
|
|
256
|
+
...DEFAULT_SETTINGS,
|
|
257
|
+
providerSettings: getProviderDefaults(),
|
|
258
|
+
};
|
|
90
259
|
}
|
|
91
260
|
try {
|
|
92
261
|
const data = fs.readFileSync(settingsFile, 'utf8');
|
|
93
|
-
|
|
262
|
+
const parsed = normalizeLoadedSettings(JSON.parse(data));
|
|
263
|
+
const merged = { ...DEFAULT_SETTINGS, ...parsed };
|
|
264
|
+
merged.defaultProvider =
|
|
265
|
+
normalizeProviderName(merged.defaultProvider) || DEFAULT_SETTINGS.defaultProvider;
|
|
266
|
+
merged.providerSettings = mergeProviderSettings(getProviderDefaults(), parsed.providerSettings);
|
|
267
|
+
return applyLegacyModelBounds(merged);
|
|
94
268
|
} catch {
|
|
95
269
|
console.error('Warning: Could not load settings, using defaults');
|
|
96
|
-
return {
|
|
270
|
+
return {
|
|
271
|
+
...DEFAULT_SETTINGS,
|
|
272
|
+
providerSettings: getProviderDefaults(),
|
|
273
|
+
};
|
|
97
274
|
}
|
|
98
275
|
}
|
|
99
276
|
|
|
@@ -109,6 +286,51 @@ function saveSettings(settings) {
|
|
|
109
286
|
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2), 'utf8');
|
|
110
287
|
}
|
|
111
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Validate claudeCommand setting
|
|
291
|
+
* @returns {string|null} Error message if invalid, null if valid
|
|
292
|
+
*/
|
|
293
|
+
function validateClaudeCommand(value) {
|
|
294
|
+
if (typeof value !== 'string') {
|
|
295
|
+
return 'claudeCommand must be a string';
|
|
296
|
+
}
|
|
297
|
+
if (value.trim().length === 0) {
|
|
298
|
+
return 'claudeCommand cannot be empty';
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Validate providerSettings structure by delegating to provider implementations
|
|
305
|
+
* @returns {string|null} Error message if invalid, null if valid
|
|
306
|
+
*/
|
|
307
|
+
function validateProviderSettings(value) {
|
|
308
|
+
const normalizedSettings = normalizeProviderSettings(value);
|
|
309
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
310
|
+
return 'providerSettings must be an object';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Lazy require to avoid circular dependency
|
|
314
|
+
const { getProvider } = require('../src/providers');
|
|
315
|
+
|
|
316
|
+
for (const [providerName, settings] of Object.entries(normalizedSettings || {})) {
|
|
317
|
+
if (!VALID_PROVIDERS.includes(providerName)) {
|
|
318
|
+
return `Unknown provider in providerSettings: ${providerName}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Delegate validation to the provider
|
|
322
|
+
try {
|
|
323
|
+
const provider = getProvider(providerName);
|
|
324
|
+
const error = provider.validateSettings(settings);
|
|
325
|
+
if (error) return error;
|
|
326
|
+
} catch (err) {
|
|
327
|
+
return `Failed to validate ${providerName} settings: ${err.message}`;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
112
334
|
/**
|
|
113
335
|
* Validate a setting value
|
|
114
336
|
* @returns {string|null} Error message if invalid, null if valid
|
|
@@ -122,19 +344,29 @@ function validateSetting(key, value) {
|
|
|
122
344
|
return `Invalid model: ${value}. Valid models: ${VALID_MODELS.join(', ')}`;
|
|
123
345
|
}
|
|
124
346
|
|
|
347
|
+
if (key === 'minModel' && value !== null && !VALID_MODELS.includes(value)) {
|
|
348
|
+
return `Invalid model: ${value}. Valid models: ${VALID_MODELS.join(', ')}, null`;
|
|
349
|
+
}
|
|
350
|
+
|
|
125
351
|
if (key === 'logLevel' && !['quiet', 'normal', 'verbose'].includes(value)) {
|
|
126
352
|
return `Invalid log level: ${value}. Valid levels: quiet, normal, verbose`;
|
|
127
353
|
}
|
|
128
354
|
|
|
129
355
|
if (key === 'claudeCommand') {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
356
|
+
return validateClaudeCommand(value);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (key === 'defaultProvider') {
|
|
360
|
+
const normalized = normalizeProviderName(value);
|
|
361
|
+
if (!VALID_PROVIDERS.includes(normalized)) {
|
|
362
|
+
return `Invalid provider: ${value}. Valid providers: ${VALID_PROVIDERS.join(', ')}`;
|
|
135
363
|
}
|
|
136
364
|
}
|
|
137
365
|
|
|
366
|
+
if (key === 'providerSettings') {
|
|
367
|
+
return validateProviderSettings(value);
|
|
368
|
+
}
|
|
369
|
+
|
|
138
370
|
if (key === 'dockerMounts') {
|
|
139
371
|
return validateMountConfig(value);
|
|
140
372
|
}
|
|
@@ -143,15 +375,49 @@ function validateSetting(key, value) {
|
|
|
143
375
|
return validateEnvPassthrough(value);
|
|
144
376
|
}
|
|
145
377
|
|
|
378
|
+
// Issue source settings validation (grouped for maintainability)
|
|
379
|
+
const issueSourceError = validateIssueSourceSetting(key, value);
|
|
380
|
+
if (issueSourceError !== undefined) {
|
|
381
|
+
return issueSourceError;
|
|
382
|
+
}
|
|
383
|
+
|
|
146
384
|
return null;
|
|
147
385
|
}
|
|
148
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Validate issue source related settings
|
|
389
|
+
* Delegates to issue providers for provider-specific settings
|
|
390
|
+
* @param {string} key - Setting key
|
|
391
|
+
* @param {any} value - Setting value
|
|
392
|
+
* @returns {string|null|undefined} Error message, null if valid, undefined if not an issue source setting
|
|
393
|
+
*/
|
|
394
|
+
function validateIssueSourceSetting(key, value) {
|
|
395
|
+
// defaultIssueSource is handled here (not provider-specific)
|
|
396
|
+
if (key === 'defaultIssueSource') {
|
|
397
|
+
const { listProviders } = getIssueProviderFns();
|
|
398
|
+
const validSources = listProviders();
|
|
399
|
+
if (!validSources.includes(value)) {
|
|
400
|
+
return `Invalid issue source: ${value}. Valid: ${validSources.join(', ')}`;
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Delegate to issue providers for provider-specific settings
|
|
406
|
+
const { validateIssueProviderSetting } = getIssueProviderFns();
|
|
407
|
+
return validateIssueProviderSetting(key, value);
|
|
408
|
+
}
|
|
409
|
+
|
|
149
410
|
/**
|
|
150
411
|
* Coerce value to correct type based on default value type
|
|
151
412
|
*/
|
|
152
413
|
function coerceValue(key, value) {
|
|
153
414
|
const defaultValue = DEFAULT_SETTINGS[key];
|
|
154
415
|
|
|
416
|
+
// Handle null values for minModel
|
|
417
|
+
if (key === 'minModel' && (value === 'null' || value === null)) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
|
|
155
421
|
if (typeof defaultValue === 'boolean') {
|
|
156
422
|
return value === 'true' || value === '1' || value === 'yes' || value === true;
|
|
157
423
|
}
|
|
@@ -183,6 +449,21 @@ function coerceValue(key, value) {
|
|
|
183
449
|
return value;
|
|
184
450
|
}
|
|
185
451
|
|
|
452
|
+
if (key === 'providerSettings') {
|
|
453
|
+
if (typeof value === 'string') {
|
|
454
|
+
try {
|
|
455
|
+
return normalizeProviderSettings(JSON.parse(value));
|
|
456
|
+
} catch {
|
|
457
|
+
throw new Error(`Invalid JSON for providerSettings: ${value}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return normalizeProviderSettings(value);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (key === 'defaultProvider') {
|
|
464
|
+
return normalizeProviderName(value);
|
|
465
|
+
}
|
|
466
|
+
|
|
186
467
|
return value;
|
|
187
468
|
}
|
|
188
469
|
|
|
@@ -213,6 +494,8 @@ module.exports = {
|
|
|
213
494
|
MODEL_HIERARCHY,
|
|
214
495
|
VALID_MODELS,
|
|
215
496
|
validateModelAgainstMax,
|
|
497
|
+
// Provider defaults exports
|
|
498
|
+
clearProviderDefaultsCache,
|
|
216
499
|
// Backward compatibility: SETTINGS_FILE as getter (reads env var dynamically)
|
|
217
500
|
get SETTINGS_FILE() {
|
|
218
501
|
return getSettingsFile();
|