@covibes/zeroshot 5.3.0 → 5.4.1

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.
Files changed (119) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +8 -385
  3. package/cli/index.js +108 -4615
  4. package/package.json +16 -125
  5. package/CHANGELOG.md +0 -537
  6. package/cli/commands/providers.js +0 -150
  7. package/cli/lib/first-run.js +0 -211
  8. package/cli/lib/update-checker.js +0 -281
  9. package/cli/message-formatter-utils.js +0 -75
  10. package/cli/message-formatters-normal.js +0 -253
  11. package/cli/message-formatters-watch.js +0 -181
  12. package/cluster-templates/base-templates/debug-workflow.json +0 -375
  13. package/cluster-templates/base-templates/full-workflow.json +0 -697
  14. package/cluster-templates/base-templates/single-worker.json +0 -63
  15. package/cluster-templates/base-templates/worker-validator.json +0 -152
  16. package/cluster-templates/conductor-bootstrap.json +0 -124
  17. package/docker/zeroshot-cluster/Dockerfile +0 -152
  18. package/docker/zeroshot-cluster/pre-baked-deps.json +0 -28
  19. package/lib/completion.js +0 -174
  20. package/lib/docker-config.js +0 -212
  21. package/lib/id-detector.js +0 -53
  22. package/lib/provider-detection.js +0 -59
  23. package/lib/provider-names.js +0 -56
  24. package/lib/settings.js +0 -405
  25. package/lib/stream-json-parser.js +0 -10
  26. package/scripts/fix-node-pty-permissions.js +0 -75
  27. package/scripts/record-demo.sh +0 -279
  28. package/scripts/test-install.sh +0 -40
  29. package/scripts/validate-templates.js +0 -100
  30. package/src/agent/agent-config.js +0 -193
  31. package/src/agent/agent-context-builder.js +0 -335
  32. package/src/agent/agent-hook-executor.js +0 -402
  33. package/src/agent/agent-lifecycle.js +0 -683
  34. package/src/agent/agent-stuck-detector.js +0 -256
  35. package/src/agent/agent-task-executor.js +0 -1431
  36. package/src/agent/agent-trigger-evaluator.js +0 -67
  37. package/src/agent/output-extraction.js +0 -200
  38. package/src/agent/output-reformatter.js +0 -175
  39. package/src/agent/schema-utils.js +0 -111
  40. package/src/agent-wrapper.js +0 -570
  41. package/src/agents/git-pusher-agent.json +0 -20
  42. package/src/attach/attach-client.js +0 -438
  43. package/src/attach/attach-server.js +0 -543
  44. package/src/attach/index.js +0 -35
  45. package/src/attach/protocol.js +0 -220
  46. package/src/attach/ring-buffer.js +0 -121
  47. package/src/attach/socket-discovery.js +0 -267
  48. package/src/claude-task-runner.js +0 -523
  49. package/src/config-router.js +0 -80
  50. package/src/config-validator.js +0 -1660
  51. package/src/copy-worker.js +0 -43
  52. package/src/github.js +0 -139
  53. package/src/isolation-manager.js +0 -1381
  54. package/src/ledger.js +0 -614
  55. package/src/logic-engine.js +0 -223
  56. package/src/message-bus-bridge.js +0 -139
  57. package/src/message-bus.js +0 -249
  58. package/src/name-generator.js +0 -232
  59. package/src/orchestrator.js +0 -2519
  60. package/src/preflight.js +0 -485
  61. package/src/process-metrics.js +0 -566
  62. package/src/providers/anthropic/cli-builder.js +0 -45
  63. package/src/providers/anthropic/index.js +0 -134
  64. package/src/providers/anthropic/models.js +0 -23
  65. package/src/providers/anthropic/output-parser.js +0 -159
  66. package/src/providers/base-provider.js +0 -181
  67. package/src/providers/capabilities.js +0 -51
  68. package/src/providers/google/cli-builder.js +0 -55
  69. package/src/providers/google/index.js +0 -116
  70. package/src/providers/google/models.js +0 -24
  71. package/src/providers/google/output-parser.js +0 -92
  72. package/src/providers/index.js +0 -75
  73. package/src/providers/openai/cli-builder.js +0 -122
  74. package/src/providers/openai/index.js +0 -135
  75. package/src/providers/openai/models.js +0 -21
  76. package/src/providers/openai/output-parser.js +0 -129
  77. package/src/schemas/sub-cluster.js +0 -156
  78. package/src/status-footer.js +0 -980
  79. package/src/sub-cluster-wrapper.js +0 -560
  80. package/src/task-runner.js +0 -30
  81. package/src/template-resolver.js +0 -369
  82. package/src/tui/CHANGES.txt +0 -133
  83. package/src/tui/LAYOUT.md +0 -261
  84. package/src/tui/README.txt +0 -192
  85. package/src/tui/TWO-LEVEL-NAVIGATION.md +0 -186
  86. package/src/tui/data-poller.js +0 -325
  87. package/src/tui/demo.js +0 -208
  88. package/src/tui/formatters.js +0 -128
  89. package/src/tui/index.js +0 -193
  90. package/src/tui/keybindings.js +0 -383
  91. package/src/tui/layout.js +0 -334
  92. package/src/tui/renderer.js +0 -194
  93. package/task-lib/attachable-watcher.js +0 -272
  94. package/task-lib/claude-recovery.js +0 -119
  95. package/task-lib/commands/clean.js +0 -50
  96. package/task-lib/commands/get-log-path.js +0 -23
  97. package/task-lib/commands/kill.js +0 -32
  98. package/task-lib/commands/list.js +0 -105
  99. package/task-lib/commands/logs.js +0 -411
  100. package/task-lib/commands/resume.js +0 -42
  101. package/task-lib/commands/run.js +0 -57
  102. package/task-lib/commands/schedule.js +0 -105
  103. package/task-lib/commands/scheduler-cmd.js +0 -96
  104. package/task-lib/commands/schedules.js +0 -98
  105. package/task-lib/commands/status.js +0 -44
  106. package/task-lib/commands/unschedule.js +0 -16
  107. package/task-lib/completion.js +0 -9
  108. package/task-lib/config.js +0 -10
  109. package/task-lib/name-generator.js +0 -230
  110. package/task-lib/package.json +0 -3
  111. package/task-lib/runner.js +0 -144
  112. package/task-lib/scheduler.js +0 -252
  113. package/task-lib/store.js +0 -230
  114. package/task-lib/tui/formatters.js +0 -166
  115. package/task-lib/tui/index.js +0 -197
  116. package/task-lib/tui/layout.js +0 -111
  117. package/task-lib/tui/renderer.js +0 -119
  118. package/task-lib/tui.js +0 -384
  119. package/task-lib/watcher.js +0 -188
@@ -1,279 +0,0 @@
1
- #!/bin/bash
2
- # record-demo.sh - Create ephemeral project, run zeroshot demo, record it, cleanup
3
- #
4
- # Usage:
5
- # ./scripts/record-demo.sh # Interactive mode (you run zeroshot manually)
6
- # ./scripts/record-demo.sh --record # Record with asciinema automatically
7
- #
8
- # Output: zeroshot-demo.cast (asciinema recording)
9
- # Convert to gif: agg zeroshot-demo.cast zeroshot-demo.gif --idle-time-limit 2
10
-
11
- set -e
12
-
13
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
- REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
15
- DEMO_DIR=""
16
- RECORD_MODE=false
17
- FORCE_MODE=false
18
- LOCK_FILE="/tmp/zeroshot-demo-recording.lock"
19
- CAST_FILE="$REPO_ROOT/zeroshot-demo.cast"
20
-
21
- # Check for existing recording session
22
- check_existing_session() {
23
- # Check lock file
24
- if [[ -f "$LOCK_FILE" ]]; then
25
- local old_pid
26
- old_pid=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
27
- if [[ -n "$old_pid" ]] && kill -0 "$old_pid" 2>/dev/null; then
28
- echo "ERROR: Recording already in progress (PID $old_pid)"
29
- echo "Kill it with: kill $old_pid"
30
- exit 1
31
- fi
32
- # Stale lock file
33
- rm -f "$LOCK_FILE"
34
- fi
35
-
36
- # Check for any asciinema processes recording to our file
37
- local existing_asciinema
38
- existing_asciinema=$(pgrep -f "asciinema.*zeroshot-demo.cast" 2>/dev/null || true)
39
- if [[ -n "$existing_asciinema" ]]; then
40
- echo "ERROR: Existing asciinema process(es) found: $existing_asciinema"
41
- echo "Kill them with: pkill -f 'asciinema.*zeroshot-demo.cast'"
42
- exit 1
43
- fi
44
-
45
- # Check for any running zeroshot clusters
46
- local existing_zeroshot
47
- existing_zeroshot=$(pgrep -f "zeroshot.*rate limiting" 2>/dev/null || true)
48
- if [[ -n "$existing_zeroshot" ]]; then
49
- echo "ERROR: Existing zeroshot process(es) found: $existing_zeroshot"
50
- echo "Kill them with: pkill -f 'zeroshot.*rate limiting'"
51
- exit 1
52
- fi
53
- }
54
-
55
- # Parse args
56
- while [[ $# -gt 0 ]]; do
57
- case "$1" in
58
- --record) RECORD_MODE=true; shift ;;
59
- --force|-f) FORCE_MODE=true; shift ;;
60
- *) echo "Unknown option: $1"; exit 1 ;;
61
- esac
62
- done
63
-
64
- # Cleanup on exit
65
- cleanup() {
66
- # Remove lock file
67
- rm -f "$LOCK_FILE"
68
-
69
- if [[ -n "$DEMO_DIR" && -d "$DEMO_DIR" ]]; then
70
- echo ""
71
- echo "Cleaning up $DEMO_DIR..."
72
- rm -rf "$DEMO_DIR"
73
- echo "Done."
74
- fi
75
- }
76
- trap cleanup EXIT
77
-
78
- # Check for conflicts before doing anything
79
- check_existing_session
80
-
81
- # Create temp project
82
- DEMO_DIR=$(mktemp -d -t zeroshot-demo-XXXXXX)
83
- echo "Creating demo project in $DEMO_DIR"
84
-
85
- cd "$DEMO_DIR"
86
-
87
- # Initialize package.json
88
- cat > package.json << 'EOF'
89
- {
90
- "name": "demo-api",
91
- "version": "1.0.0",
92
- "type": "commonjs",
93
- "scripts": {
94
- "dev": "ts-node src/index.ts",
95
- "build": "tsc",
96
- "start": "node dist/index.js"
97
- }
98
- }
99
- EOF
100
-
101
- # Install dependencies (quiet)
102
- echo "Installing dependencies..."
103
- npm install --silent express typescript ts-node @types/express @types/node
104
-
105
- # Create tsconfig
106
- cat > tsconfig.json << 'EOF'
107
- {
108
- "compilerOptions": {
109
- "target": "ES2020",
110
- "module": "commonjs",
111
- "strict": true,
112
- "esModuleInterop": true,
113
- "outDir": "dist",
114
- "rootDir": "src"
115
- },
116
- "include": ["src/**/*"]
117
- }
118
- EOF
119
-
120
- # Create minimal server with users "database"
121
- mkdir -p src
122
-
123
- cat > src/db.ts << 'EOF'
124
- // Simple in-memory database
125
- export interface User {
126
- id: number;
127
- username: string;
128
- email: string;
129
- password_hash: string;
130
- avatar: string;
131
- created_at: Date;
132
- }
133
-
134
- export const users: User[] = [
135
- {
136
- id: 1,
137
- username: "alice",
138
- email: "alice@example.com",
139
- password_hash: "$2b$10$X7VYKzPQ...",
140
- avatar: "https://example.com/alice.jpg",
141
- created_at: new Date("2024-01-15"),
142
- },
143
- {
144
- id: 2,
145
- username: "bob",
146
- email: "bob@example.com",
147
- password_hash: "$2b$10$Y8WZLaQR...",
148
- avatar: "https://example.com/bob.jpg",
149
- created_at: new Date("2024-02-20"),
150
- },
151
- ];
152
- EOF
153
-
154
- cat > src/index.ts << 'EOF'
155
- import express from "express";
156
-
157
- const app = express();
158
- const PORT = 3000;
159
-
160
- app.use(express.json());
161
-
162
- // Health check
163
- app.get("/health", (_, res) => {
164
- res.json({ status: "ok" });
165
- });
166
-
167
- app.listen(PORT, () => {
168
- console.log(`Server running on http://localhost:${PORT}`);
169
- });
170
- EOF
171
-
172
- # Initialize git repo
173
- git init --quiet
174
- git add -A
175
- git commit --quiet -m "Initial commit: Express API with users database"
176
-
177
- echo ""
178
- echo "=========================================="
179
- echo "Demo project ready!"
180
- echo "=========================================="
181
- echo ""
182
- echo "Directory: $DEMO_DIR"
183
- echo ""
184
- echo "Suggested demo task:"
185
- echo " zeroshot 'Add PUT /users/:id endpoint to update user profile'"
186
- echo ""
187
-
188
- if [[ "$RECORD_MODE" == "true" ]]; then
189
- echo "Recording with asciinema..."
190
- echo ""
191
-
192
- # Check asciinema is installed
193
- if ! command -v asciinema &> /dev/null; then
194
- echo "Error: asciinema not installed. Run: pip install asciinema"
195
- exit 1
196
- fi
197
-
198
- # Warn if cast file exists (skip with --force)
199
- if [[ -f "$CAST_FILE" ]] && [[ "$FORCE_MODE" != "true" ]]; then
200
- echo "WARNING: $CAST_FILE already exists!"
201
- echo "Previous recording will be OVERWRITTEN."
202
- echo ""
203
- read -p "Continue? [y/N] " -n 1 -r
204
- echo ""
205
- if [[ ! $REPLY =~ ^[Yy]$ ]]; then
206
- echo "Aborted."
207
- exit 1
208
- fi
209
- fi
210
-
211
- # Create lock file with our PID
212
- echo $$ > "$LOCK_FILE"
213
-
214
- # Record the session
215
- # CRITICAL: Use bash -l -c to get full PATH (includes npm global binaries)
216
- ZEROSHOT_PATH=$(which zeroshot)
217
- TASK="Add rate limiting middleware: sliding window algorithm (not fixed window), per-IP tracking with in-memory store and automatic TTL cleanup to prevent memory leaks, configurable limits per endpoint. Return 429 Too Many Requests with Retry-After header (seconds until reset) and X-RateLimit-Remaining header on ALL responses. Must handle both IPv4 and IPv6, normalizing IPv6 to consistent format."
218
-
219
- # SIGNAL ISOLATION: Use setsid to create new session, completely immune to terminal signals
220
- # The recording process will NOT be killed by Ctrl+C or signals to this script
221
- # We wait for it explicitly and only cleanup AFTER it naturally completes
222
- echo "Starting recording in isolated session (immune to Ctrl+C)..."
223
- echo "To kill it manually: pkill -f 'asciinema.*zeroshot-demo.cast'"
224
- echo ""
225
-
226
- # Disable cleanup trap during recording - we'll handle it manually
227
- trap - EXIT
228
-
229
- # Start asciinema in new session (setsid) so it's immune to our signals
230
- # Save the session leader PID so we can wait for it
231
- setsid bash -c "
232
- # Ignore all signals - this recording WILL NOT DIE
233
- trap '' INT TERM HUP QUIT
234
-
235
- cd '$DEMO_DIR'
236
- asciinema rec \
237
- --overwrite \
238
- --title 'Zeroshot Demo' \
239
- --command \"bash -l -c '$ZEROSHOT_PATH \\\"$TASK\\\"'\" \
240
- '$CAST_FILE'
241
- " &
242
- RECORDING_PID=$!
243
-
244
- # Update lock file with the actual recording PID
245
- echo $RECORDING_PID > "$LOCK_FILE"
246
-
247
- echo "Recording started (session PID: $RECORDING_PID)"
248
- echo "Waiting for recording to complete..."
249
- echo ""
250
-
251
- # Wait for recording to finish - this script will NOT kill it on Ctrl+C
252
- # Ignore signals while waiting
253
- trap '' INT TERM HUP
254
- wait $RECORDING_PID 2>/dev/null || true
255
-
256
- # Recording finished naturally - now cleanup
257
- echo ""
258
- echo "Recording saved to: $CAST_FILE"
259
- echo ""
260
- echo "Convert to gif with:"
261
- echo " agg $CAST_FILE $REPO_ROOT/zeroshot-demo.gif --idle-time-limit 2"
262
-
263
- # Now do cleanup
264
- rm -f "$LOCK_FILE"
265
- if [[ -n "$DEMO_DIR" && -d "$DEMO_DIR" ]]; then
266
- echo ""
267
- echo "Cleaning up $DEMO_DIR..."
268
- rm -rf "$DEMO_DIR"
269
- echo "Done."
270
- fi
271
- else
272
- echo "Interactive mode - run zeroshot manually:"
273
- echo ""
274
- echo " cd $DEMO_DIR"
275
- echo " zeroshot 'Add PUT /users/:id endpoint to update user profile'"
276
- echo ""
277
- echo "Press Enter when done to cleanup..."
278
- read -r
279
- fi
@@ -1,40 +0,0 @@
1
- #!/bin/bash
2
- # Test zeroshot installation on any platform
3
- # Run: ./scripts/test-install.sh
4
-
5
- set -e
6
-
7
- echo "=== Zeroshot Install Test ==="
8
- echo "Platform: $(uname -s) $(uname -m)"
9
- echo "Node: $(node --version)"
10
- echo "npm: $(npm --version)"
11
- echo ""
12
-
13
- echo "1. Installing dependencies..."
14
- npm install
15
-
16
- echo ""
17
- echo "2. Linking CLI..."
18
- npm link
19
-
20
- echo ""
21
- echo "3. Testing CLI commands..."
22
-
23
- echo " zeroshot --version"
24
- zeroshot --version
25
-
26
- echo ""
27
- echo " zeroshot --help"
28
- zeroshot --help | head -20
29
-
30
- echo ""
31
- echo " zeroshot list"
32
- zeroshot list 2>/dev/null || echo " (no clusters yet - this is expected)"
33
-
34
- echo ""
35
- echo " zeroshot config list"
36
- zeroshot config list
37
-
38
- echo ""
39
- echo "=== ALL TESTS PASSED ==="
40
- echo "Zeroshot installed successfully on $(uname -s)!"
@@ -1,100 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Validate all cluster templates for config errors
4
- * Run in CI to prevent broken templates from being merged
5
- *
6
- * Usage: node scripts/validate-templates.js
7
- * Exit codes: 0 = all valid, 1 = validation errors found
8
- */
9
-
10
- const fs = require('fs');
11
- const path = require('path');
12
- const { validateConfig } = require('../src/config-validator');
13
-
14
- const TEMPLATES_DIR = path.join(__dirname, '../cluster-templates');
15
-
16
- function findJsonFiles(dir) {
17
- const files = [];
18
- if (!fs.existsSync(dir)) return files;
19
-
20
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
21
- const fullPath = path.join(dir, entry.name);
22
- if (entry.isDirectory()) {
23
- files.push(...findJsonFiles(fullPath));
24
- } else if (entry.name.endsWith('.json')) {
25
- files.push(fullPath);
26
- }
27
- }
28
- return files;
29
- }
30
-
31
- function validateTemplate(filePath) {
32
- const relativePath = path.relative(process.cwd(), filePath);
33
-
34
- try {
35
- const content = fs.readFileSync(filePath, 'utf-8');
36
- const config = JSON.parse(content);
37
-
38
- // Skip non-cluster configs (like package.json)
39
- if (!config.agents && !config.name) {
40
- return { valid: true, skipped: true };
41
- }
42
-
43
- const result = validateConfig(config);
44
-
45
- if (!result.valid) {
46
- console.error(`\n❌ ${relativePath}`);
47
- for (const error of result.errors) {
48
- console.error(` ERROR: ${error}`);
49
- }
50
- } else if (result.warnings.length > 0) {
51
- console.warn(`\n⚠️ ${relativePath}`);
52
- for (const warning of result.warnings) {
53
- console.warn(` WARN: ${warning}`);
54
- }
55
- } else {
56
- console.log(`✓ ${relativePath}`);
57
- }
58
-
59
- return result;
60
- } catch (err) {
61
- console.error(`\n❌ ${relativePath}`);
62
- console.error(` PARSE ERROR: ${err.message}`);
63
- return { valid: false, errors: [err.message], warnings: [] };
64
- }
65
- }
66
-
67
- function main() {
68
- console.log('Validating cluster templates...\n');
69
-
70
- const templateFiles = [...findJsonFiles(TEMPLATES_DIR)];
71
-
72
- let hasErrors = false;
73
- let validated = 0;
74
- let skipped = 0;
75
-
76
- for (const file of templateFiles) {
77
- const result = validateTemplate(file);
78
- if (result.skipped) {
79
- skipped++;
80
- } else {
81
- validated++;
82
- if (!result.valid) {
83
- hasErrors = true;
84
- }
85
- }
86
- }
87
-
88
- console.log(`\n${'='.repeat(60)}`);
89
- console.log(`Validated: ${validated} templates, Skipped: ${skipped} files`);
90
-
91
- if (hasErrors) {
92
- console.error('\n❌ VALIDATION FAILED - Fix errors above before merging\n');
93
- process.exit(1);
94
- } else {
95
- console.log('\n✓ All templates valid\n');
96
- process.exit(0);
97
- }
98
- }
99
-
100
- main();
@@ -1,193 +0,0 @@
1
- /**
2
- * AgentConfig - Agent configuration validation and defaults
3
- *
4
- * Provides:
5
- * - Config validation and normalization
6
- * - Default values for optional fields
7
- * - Model configuration setup
8
- * - Safety checks for test mode
9
- * - maxModel ceiling enforcement at config time
10
- */
11
-
12
- const { loadSettings, validateModelAgainstMax, VALID_MODELS } = require('../../lib/settings');
13
-
14
- const VALID_LEVELS = ['level1', 'level2', 'level3'];
15
-
16
- // Default max iterations (high limit - let the user decide when to give up)
17
- const DEFAULT_MAX_ITERATIONS = 100;
18
-
19
- // Default timeout: 0 = no timeout (task runs until completion or explicit kill)
20
- // Use positive number for timeout in milliseconds
21
- const DEFAULT_TIMEOUT = 0;
22
-
23
- // Stale detection - ENABLED by default using multi-indicator analysis (safe from false positives)
24
- // Multi-indicator approach checks: process state, CPU usage, context switches, network I/O
25
- // Only flags as stuck when ALL indicators show inactivity (score >= 3.5)
26
- // Single-indicator detection (just output freshness) was too risky - this is safe.
27
- const DEFAULT_STALE_DURATION_MS = 30 * 60 * 1000; // 30 minutes before triggering analysis
28
- const DEFAULT_LIVENESS_CHECK_ENABLED = true; // Safe with multi-indicator detection
29
-
30
- /**
31
- * Validate and normalize agent configuration
32
- * @param {Object} config - Raw agent configuration
33
- * @param {Object} options - Agent wrapper options
34
- * @returns {Object} Normalized configuration
35
- */
36
- function validateAgentConfig(config, options = {}) {
37
- // CRITICAL: Enforce JSON schema output by default to prevent parse failures and crashes
38
- // Agents MUST return structured output so hooks can safely use {{result.*}} templates
39
- if (!config.outputFormat) {
40
- config.outputFormat = 'json';
41
- }
42
-
43
- // If outputFormat is json but no schema defined, use a minimal default schema
44
- // This prevents uncaught exceptions when parsing agent output
45
- if (config.outputFormat === 'json' && !config.jsonSchema) {
46
- config.jsonSchema = {
47
- type: 'object',
48
- properties: {
49
- summary: {
50
- type: 'string',
51
- description: 'Brief summary of what was done',
52
- },
53
- result: {
54
- type: 'string',
55
- description: 'Detailed result or output',
56
- },
57
- },
58
- required: ['summary', 'result'],
59
- };
60
- }
61
-
62
- // Model configuration: support both static model and dynamic rules
63
- // If no model specified, model is null - _selectModel() will use provider defaults
64
- let modelConfig;
65
- if (config.modelRules) {
66
- modelConfig = { type: 'rules', rules: config.modelRules };
67
- } else {
68
- modelConfig = {
69
- type: 'static',
70
- model: config.model || null,
71
- modelLevel: config.modelLevel || null,
72
- };
73
- }
74
-
75
- // COST CEILING/FLOOR ENFORCEMENT: Validate model(s) against maxModel and minModel at config time
76
- // Catches violations EARLY (config load) instead of at runtime (iteration N)
77
- const settings = loadSettings();
78
- const maxModel = settings.maxModel || 'sonnet';
79
- const minModel = settings.minModel || null;
80
-
81
- if (modelConfig.type === 'static') {
82
- if (modelConfig.model && VALID_MODELS.includes(modelConfig.model)) {
83
- // Static model: validate once (legacy Claude models only)
84
- try {
85
- validateModelAgainstMax(modelConfig.model, maxModel, minModel);
86
- } catch (error) {
87
- throw new Error(`Agent "${config.id}": ${error.message}`);
88
- }
89
- }
90
-
91
- if (modelConfig.modelLevel && !VALID_LEVELS.includes(modelConfig.modelLevel)) {
92
- throw new Error(
93
- `Agent "${config.id}": invalid modelLevel "${modelConfig.modelLevel}". ` +
94
- `Valid: ${VALID_LEVELS.join(', ')}`
95
- );
96
- }
97
- } else if (modelConfig.type === 'rules') {
98
- // Dynamic rules: validate ALL rules upfront (don't wait until iteration N)
99
- for (const rule of modelConfig.rules) {
100
- if (rule.model && VALID_MODELS.includes(rule.model)) {
101
- try {
102
- validateModelAgainstMax(rule.model, maxModel, minModel);
103
- } catch {
104
- throw new Error(
105
- `Agent "${config.id}": modelRule "${rule.iterations}" requests "${rule.model}" ` +
106
- `but maxModel is "${maxModel}"${minModel ? ` and minModel is "${minModel}"` : ''}. ` +
107
- `Either adjust the rule's model or change maxModel/minModel settings.`
108
- );
109
- }
110
- }
111
-
112
- if (rule.modelLevel && !VALID_LEVELS.includes(rule.modelLevel)) {
113
- throw new Error(
114
- `Agent "${config.id}": modelRule "${rule.iterations}" has invalid modelLevel ` +
115
- `"${rule.modelLevel}". Valid: ${VALID_LEVELS.join(', ')}`
116
- );
117
- }
118
- }
119
- }
120
-
121
- // Prompt configuration: support static prompt OR iteration-based rules
122
- // Formats:
123
- // prompt: "string" -> static
124
- // prompt: { system: "string" } -> static
125
- // prompt: { initial: "...", subsequent: "..." } -> iteration 1 vs 2+
126
- // prompt: { iterations: [...] } -> full control
127
- let promptConfig = null;
128
- if (config.prompt?.iterations) {
129
- promptConfig = { type: 'rules', rules: config.prompt.iterations };
130
- } else if (config.prompt?.initial || config.prompt?.subsequent) {
131
- const rules = [];
132
- if (config.prompt.initial) rules.push({ match: '1', system: config.prompt.initial });
133
- if (config.prompt.subsequent) rules.push({ match: '2+', system: config.prompt.subsequent });
134
- promptConfig = { type: 'rules', rules };
135
- } else if (typeof config.prompt === 'string') {
136
- promptConfig = { type: 'static', system: config.prompt };
137
- } else if (config.prompt?.system) {
138
- promptConfig = { type: 'static', system: config.prompt.system };
139
- } else if (config.prompt) {
140
- throw new Error(`Agent "${config.id}": invalid prompt format`);
141
- }
142
-
143
- // Default timeout to 0 (no timeout) if not specified
144
- // Use positive number for timeout in milliseconds
145
- // ROBUST: Handle undefined, null, AND string values from template resolution
146
- if (config.timeout === undefined || config.timeout === null || config.timeout === '') {
147
- config.timeout = DEFAULT_TIMEOUT;
148
- } else {
149
- // Coerce to number (handles string "0" from template resolution)
150
- config.timeout = Number(config.timeout);
151
- }
152
- if (!Number.isFinite(config.timeout) || config.timeout < 0) {
153
- throw new Error(
154
- `Agent "${config.id}": timeout must be a non-negative number (got ${config.timeout}).`
155
- );
156
- }
157
-
158
- // Build normalized config
159
- const normalizedConfig = {
160
- ...config,
161
- modelConfig,
162
- promptConfig,
163
- maxIterations: config.maxIterations || DEFAULT_MAX_ITERATIONS,
164
- timeout: config.timeout, // Defaults to 0 (no timeout) if not specified
165
- staleDuration: config.staleDuration || DEFAULT_STALE_DURATION_MS,
166
- enableLivenessCheck: config.enableLivenessCheck ?? DEFAULT_LIVENESS_CHECK_ENABLED, // On by default, opt-out with false
167
- };
168
-
169
- // SAFETY: In test mode, verify mock is provided for agents that execute tasks
170
- // Check if this agent executes tasks (vs orchestrator agents that only publish messages)
171
- const executesTask = config.triggers?.some(
172
- (trigger) => !trigger.action || trigger.action === 'execute_task'
173
- );
174
-
175
- // Accept either mockSpawnFn OR taskRunner as valid mock sources
176
- const hasMock = options.mockSpawnFn || options.taskRunner;
177
-
178
- if (options.testMode && !hasMock && executesTask) {
179
- throw new Error(
180
- `AgentWrapper: testMode=true but no mockSpawnFn/taskRunner provided for agent '${config.id}'. ` +
181
- `This would cause real Claude API calls. ABORTING.`
182
- );
183
- }
184
-
185
- return normalizedConfig;
186
- }
187
-
188
- module.exports = {
189
- validateAgentConfig,
190
- DEFAULT_MAX_ITERATIONS,
191
- DEFAULT_STALE_DURATION_MS,
192
- DEFAULT_LIVENESS_CHECK_ENABLED,
193
- };