@soederpop/luca 0.0.5 → 0.0.7
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/CLAUDE.md +10 -1
- package/bun.lock +1 -1
- package/commands/build-bootstrap.ts +78 -0
- package/commands/build-scaffolds.ts +24 -2
- package/commands/try-all-challenges.ts +543 -0
- package/commands/try-challenge.ts +100 -0
- package/docs/README.md +52 -80
- package/docs/TABLE-OF-CONTENTS.md +82 -51
- package/docs/apis/clients/elevenlabs.md +232 -8
- package/docs/apis/clients/graph.md +59 -8
- package/docs/apis/clients/openai.md +362 -2
- package/docs/apis/clients/rest.md +122 -2
- package/docs/apis/clients/websocket.md +71 -17
- package/docs/apis/features/agi/assistant.md +9 -3
- package/docs/apis/features/agi/assistants-manager.md +2 -2
- package/docs/apis/features/agi/claude-code.md +153 -14
- package/docs/apis/features/agi/conversation-history.md +15 -3
- package/docs/apis/features/agi/conversation.md +133 -20
- package/docs/apis/features/agi/openai-codex.md +90 -12
- package/docs/apis/features/agi/skills-library.md +23 -5
- package/docs/apis/features/node/container-link.md +59 -0
- package/docs/apis/features/node/content-db.md +1 -1
- package/docs/apis/features/node/disk-cache.md +1 -1
- package/docs/apis/features/node/dns.md +1 -0
- package/docs/apis/features/node/docker.md +2 -1
- package/docs/apis/features/node/esbuild.md +4 -3
- package/docs/apis/features/node/file-manager.md +13 -4
- package/docs/apis/features/node/fs.md +726 -171
- package/docs/apis/features/node/git.md +1 -0
- package/docs/apis/features/node/google-auth.md +23 -4
- package/docs/apis/features/node/google-calendar.md +14 -2
- package/docs/apis/features/node/google-docs.md +15 -2
- package/docs/apis/features/node/google-drive.md +21 -3
- package/docs/apis/features/node/google-sheets.md +14 -2
- package/docs/apis/features/node/grep.md +2 -0
- package/docs/apis/features/node/helpers.md +29 -0
- package/docs/apis/features/node/ink.md +2 -2
- package/docs/apis/features/node/networking.md +39 -4
- package/docs/apis/features/node/os.md +28 -0
- package/docs/apis/features/node/postgres.md +26 -4
- package/docs/apis/features/node/proc.md +37 -28
- package/docs/apis/features/node/process-manager.md +33 -5
- package/docs/apis/features/node/repl.md +1 -1
- package/docs/apis/features/node/runpod.md +1 -0
- package/docs/apis/features/node/secure-shell.md +7 -0
- package/docs/apis/features/node/semantic-search.md +12 -5
- package/docs/apis/features/node/sqlite.md +26 -4
- package/docs/apis/features/node/telegram.md +30 -5
- package/docs/apis/features/node/tts.md +17 -2
- package/docs/apis/features/node/ui.md +1 -1
- package/docs/apis/features/node/vault.md +4 -9
- package/docs/apis/features/node/vm.md +3 -12
- package/docs/apis/features/node/window-manager.md +128 -20
- package/docs/apis/features/web/asset-loader.md +13 -1
- package/docs/apis/features/web/container-link.md +59 -0
- package/docs/apis/features/web/esbuild.md +4 -3
- package/docs/apis/features/web/helpers.md +29 -0
- package/docs/apis/features/web/network.md +16 -2
- package/docs/apis/features/web/speech.md +16 -2
- package/docs/apis/features/web/vault.md +4 -9
- package/docs/apis/features/web/vm.md +3 -12
- package/docs/apis/features/web/voice.md +18 -1
- package/docs/apis/servers/express.md +18 -2
- package/docs/apis/servers/mcp.md +29 -4
- package/docs/apis/servers/websocket.md +34 -6
- package/docs/bootstrap/CLAUDE.md +100 -0
- package/docs/bootstrap/SKILL.md +222 -0
- package/docs/bootstrap/templates/about-command.ts +41 -0
- package/docs/bootstrap/templates/docs-models.ts +22 -0
- package/docs/bootstrap/templates/docs-readme.md +43 -0
- package/docs/bootstrap/templates/example-feature.ts +53 -0
- package/docs/bootstrap/templates/health-endpoint.ts +15 -0
- package/docs/bootstrap/templates/luca-cli.ts +25 -0
- package/docs/challenges/caching-proxy.md +16 -0
- package/docs/challenges/content-db-round-trip.md +14 -0
- package/docs/challenges/custom-command.md +9 -0
- package/docs/challenges/file-watcher-pipeline.md +11 -0
- package/docs/challenges/grep-audit-report.md +15 -0
- package/docs/challenges/multi-feature-dashboard.md +14 -0
- package/docs/challenges/process-orchestrator.md +17 -0
- package/docs/challenges/rest-api-server-with-client.md +12 -0
- package/docs/challenges/script-runner-with-vm.md +11 -0
- package/docs/challenges/simple-rest-api.md +15 -0
- package/docs/challenges/websocket-serve-and-client.md +11 -0
- package/docs/challenges/yaml-config-system.md +14 -0
- package/docs/command-system-overhaul.md +94 -0
- package/docs/examples/assistant/CORE.md +18 -0
- package/docs/examples/assistant/hooks.ts +3 -0
- package/docs/examples/assistant/tools.ts +10 -0
- package/docs/examples/window-manager-layouts.md +180 -0
- package/docs/in-memory-fs.md +4 -0
- package/docs/models.ts +13 -10
- package/docs/philosophy.md +4 -3
- package/docs/reports/console-hmr-design.md +170 -0
- package/docs/reports/helper-semantic-search.md +72 -0
- package/docs/scaffolds/client.md +29 -20
- package/docs/scaffolds/command.md +64 -50
- package/docs/scaffolds/endpoint.md +31 -36
- package/docs/scaffolds/feature.md +28 -18
- package/docs/scaffolds/selector.md +91 -0
- package/docs/scaffolds/server.md +18 -9
- package/docs/selectors.md +115 -0
- package/docs/sessions/custom-command/attempt-log-2.md +195 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
- package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
- package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
- package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
- package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
- package/docs/tutorials/00-bootstrap.md +148 -0
- package/docs/tutorials/07-endpoints.md +7 -7
- package/docs/tutorials/08-commands.md +153 -72
- package/luca.cli.ts +3 -0
- package/package.json +6 -5
- package/public/index.html +1430 -0
- package/scripts/examples/using-ollama.ts +2 -1
- package/scripts/update-introspection-data.ts +2 -2
- package/src/agi/endpoints/experts.ts +1 -1
- package/src/agi/features/assistant.ts +7 -0
- package/src/agi/features/assistants-manager.ts +5 -5
- package/src/agi/features/claude-code.ts +263 -3
- package/src/agi/features/conversation-history.ts +7 -1
- package/src/agi/features/conversation.ts +26 -3
- package/src/agi/features/openai-codex.ts +26 -2
- package/src/agi/features/openapi.ts +6 -1
- package/src/agi/features/skills-library.ts +9 -1
- package/src/bootstrap/generated.ts +540 -0
- package/src/cli/cli.ts +64 -21
- package/src/client.ts +23 -357
- package/src/clients/civitai/index.ts +1 -1
- package/src/clients/client-template.ts +1 -1
- package/src/clients/comfyui/index.ts +13 -2
- package/src/clients/elevenlabs/index.ts +2 -1
- package/src/clients/graph.ts +87 -0
- package/src/clients/openai/index.ts +10 -1
- package/src/clients/rest.ts +207 -0
- package/src/clients/websocket.ts +176 -0
- package/src/command.ts +281 -34
- package/src/commands/bootstrap.ts +181 -0
- package/src/commands/chat.ts +5 -4
- package/src/commands/describe.ts +225 -2
- package/src/commands/help.ts +35 -9
- package/src/commands/index.ts +3 -0
- package/src/commands/introspect.ts +92 -2
- package/src/commands/prompt.ts +5 -6
- package/src/commands/run.ts +33 -10
- package/src/commands/save-api-docs.ts +49 -0
- package/src/commands/scaffold.ts +169 -23
- package/src/commands/select.ts +94 -0
- package/src/commands/serve.ts +10 -1
- package/src/container.ts +15 -0
- package/src/endpoint.ts +19 -0
- package/src/graft.ts +181 -0
- package/src/introspection/generated.agi.ts +12458 -8968
- package/src/introspection/generated.node.ts +10573 -7145
- package/src/introspection/generated.web.ts +1 -1
- package/src/introspection/index.ts +26 -0
- package/src/node/container.ts +6 -7
- package/src/node/features/content-db.ts +49 -2
- package/src/node/features/disk-cache.ts +16 -9
- package/src/node/features/dns.ts +16 -3
- package/src/node/features/docker.ts +16 -4
- package/src/node/features/esbuild.ts +20 -0
- package/src/node/features/file-manager.ts +184 -29
- package/src/node/features/fs.ts +704 -248
- package/src/node/features/git.ts +21 -8
- package/src/node/features/grep.ts +23 -3
- package/src/node/features/helpers.ts +372 -43
- package/src/node/features/networking.ts +39 -4
- package/src/node/features/opener.ts +28 -15
- package/src/node/features/os.ts +76 -0
- package/src/node/features/port-exposer.ts +11 -1
- package/src/node/features/postgres.ts +17 -1
- package/src/node/features/proc.ts +4 -1
- package/src/node/features/python.ts +63 -14
- package/src/node/features/repl.ts +11 -7
- package/src/node/features/runpod.ts +16 -3
- package/src/node/features/secure-shell.ts +27 -2
- package/src/node/features/semantic-search.ts +12 -1
- package/src/node/features/ui.ts +5 -69
- package/src/node/features/vm.ts +17 -0
- package/src/node/features/window-manager.ts +68 -20
- package/src/node.ts +5 -0
- package/src/scaffolds/generated.ts +492 -290
- package/src/scaffolds/template.ts +9 -0
- package/src/schemas/base.ts +46 -5
- package/src/selector.ts +282 -0
- package/src/server.ts +11 -0
- package/src/servers/express.ts +27 -12
- package/src/servers/socket.ts +45 -11
- package/src/web/clients/socket.ts +4 -1
- package/src/web/container.ts +2 -1
- package/src/web/features/network.ts +7 -1
- package/src/web/features/voice-recognition.ts +16 -1
- package/test/clients-servers.test.ts +2 -1
- package/test/command.test.ts +267 -0
- package/test-integration/assistants-manager.test.ts +10 -20
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/1b/b5/c75b28794f00f94c4d609a98978e9420e9b7146d204a7fbf5b0b30477292581705d207c0100dabaac27eef540aaaece3374af75104a93219d4ec8bfb44e7 +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/da/df/1d90ce4e042abeb035a197832c6d6893420a747a056be773eb00e4f745a037d505c8db13dde7d36b36b6b893addbb7df0f5fe9f0c13e665f20056447318b +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/ed/04/e1d0c2a58c2db29b3921ca2affb3ea4febe831c53b38ebc21019fb799823aba6ed5b4611873d2cd25d422d49955b852a9c326da0d678899bc1c2c2960901 +1 -0
- package/tmp/.cache/luca-disk-cache/index-v5/00/13/572aa4c9a94f99eda999695d050cdd0ca7fe2d23a50af03234d4c8ce0791 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/75/a9/cb61dc0f0589e8ec10a9aca27b834bc73884c479941042d22a2b22324cd3 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/9f/0f/8b1f915ee64cfff7667dd96acd7a5ac0a96aa91a346e19cefd45909a9c9c +2 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +0 -145
- package/docs/examples/launcher-app-command-listener.md +0 -120
- package/docs/tasks/web-container-helper-discovery.md +0 -71
- package/docs/todos.md +0 -1
- package/scripts/test-command-listener.ts +0 -123
- package/src/node/features/launcher-app-command-listener.ts +0 -389
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { commands, CommandOptionsSchema } from '@soederpop/luca'
|
|
3
|
+
import type { ContainerContext } from '@soederpop/luca'
|
|
4
|
+
|
|
5
|
+
declare module '@soederpop/luca' {
|
|
6
|
+
interface AvailableCommands {
|
|
7
|
+
tryAllChallenges: ReturnType<typeof commands.registerHandler>
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const argsSchema = CommandOptionsSchema.extend({
|
|
12
|
+
'batch-size': z.number().default(4).describe('Number of challenges to run in parallel per batch'),
|
|
13
|
+
'time-limit': z.number().optional().describe('Override time limit in minutes for all challenges'),
|
|
14
|
+
'dry-run': z.boolean().default(false).describe('List the batch schedule without running anything'),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
type ChallengeStatus = 'queued' | 'bootstrapping' | 'running' | 'done' | 'failed' | 'timeout'
|
|
20
|
+
|
|
21
|
+
interface ChallengeState {
|
|
22
|
+
id: string
|
|
23
|
+
slug: string
|
|
24
|
+
title: string
|
|
25
|
+
status: ChallengeStatus
|
|
26
|
+
startTime: number
|
|
27
|
+
durationMs: number
|
|
28
|
+
timeLimitMinutes: number
|
|
29
|
+
lastActivity: string
|
|
30
|
+
activityLines: string[]
|
|
31
|
+
lessonsWritten: boolean
|
|
32
|
+
attemptFolder: string
|
|
33
|
+
error: string | undefined
|
|
34
|
+
batchIndex: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
40
|
+
const MAX_ACTIVITY_LINES = 50
|
|
41
|
+
const STRIP_ANSI = /\x1b\[[0-9;]*m/g
|
|
42
|
+
|
|
43
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function formatElapsed(ms: number): string {
|
|
46
|
+
const s = Math.floor(ms / 1000)
|
|
47
|
+
const m = Math.floor(s / 60)
|
|
48
|
+
const sec = s % 60
|
|
49
|
+
return `${m}:${String(sec).padStart(2, '0')}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function pushActivity(cs: ChallengeState, line: string) {
|
|
53
|
+
const clean = line.replace(STRIP_ANSI, '').trim()
|
|
54
|
+
if (!clean) return
|
|
55
|
+
cs.activityLines.push(clean)
|
|
56
|
+
if (cs.activityLines.length > MAX_ACTIVITY_LINES) {
|
|
57
|
+
cs.activityLines = cs.activityLines.slice(-MAX_ACTIVITY_LINES)
|
|
58
|
+
}
|
|
59
|
+
cs.lastActivity = clean.slice(0, 80)
|
|
60
|
+
if (/lessons\.md/i.test(clean)) {
|
|
61
|
+
cs.lessonsWritten = true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Orchestration ──────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
async function bootstrapFolder(container: any, folder: string) {
|
|
68
|
+
const result = await container.proc.spawnAndCapture('luca', ['bootstrap'], {
|
|
69
|
+
cwd: container.paths.resolve(folder),
|
|
70
|
+
onOutput: () => {},
|
|
71
|
+
onError: () => {},
|
|
72
|
+
})
|
|
73
|
+
if (result.exitCode !== 0) {
|
|
74
|
+
throw new Error(`bootstrap failed (exit ${result.exitCode}): ${result.stderr}`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Track active child processes for cleanup on abort
|
|
79
|
+
const activeChildProcesses = new Set<any>()
|
|
80
|
+
|
|
81
|
+
async function runChallenge(
|
|
82
|
+
cs: ChallengeState,
|
|
83
|
+
container: any,
|
|
84
|
+
sessionFolder: string,
|
|
85
|
+
abortSignal: { aborted: boolean },
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
if (abortSignal.aborted) return
|
|
88
|
+
|
|
89
|
+
const fs = container.feature('fs')
|
|
90
|
+
const attemptFolder = `${sessionFolder}/${cs.slug}`
|
|
91
|
+
cs.attemptFolder = attemptFolder
|
|
92
|
+
fs.ensureFolder(attemptFolder)
|
|
93
|
+
|
|
94
|
+
// Bootstrap
|
|
95
|
+
cs.status = 'bootstrapping'
|
|
96
|
+
cs.startTime = Date.now()
|
|
97
|
+
cs.lastActivity = 'bootstrapping...'
|
|
98
|
+
await bootstrapFolder(container, attemptFolder)
|
|
99
|
+
|
|
100
|
+
if (abortSignal.aborted) return
|
|
101
|
+
|
|
102
|
+
// Run claude via luca prompt
|
|
103
|
+
cs.status = 'running'
|
|
104
|
+
cs.lastActivity = 'claude starting...'
|
|
105
|
+
|
|
106
|
+
const promptArgs = [
|
|
107
|
+
'prompt', 'claude', `docs/${cs.id}`,
|
|
108
|
+
'--exclude-sections', 'Internal Notes',
|
|
109
|
+
'--out-file', `${sessionFolder}/logs/${cs.slug}-session.md`,
|
|
110
|
+
'--in-folder', attemptFolder,
|
|
111
|
+
'--dont-touch-file',
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
const timeLimitMs = cs.timeLimitMinutes * 60 * 1000
|
|
115
|
+
|
|
116
|
+
const promptProcess = container.proc.spawnAndCapture('luca', promptArgs, {
|
|
117
|
+
onStart: (childProcess: any) => {
|
|
118
|
+
activeChildProcesses.add(childProcess)
|
|
119
|
+
},
|
|
120
|
+
onOutput: (str: string) => {
|
|
121
|
+
for (const line of str.split('\n')) {
|
|
122
|
+
pushActivity(cs, line)
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
onError: (str: string) => {
|
|
126
|
+
for (const line of str.split('\n')) {
|
|
127
|
+
pushActivity(cs, line)
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const timeout = new Promise<'timeout'>((resolve) => {
|
|
133
|
+
setTimeout(() => resolve('timeout'), timeLimitMs + 30_000)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const result = await Promise.race([
|
|
137
|
+
promptProcess.then(() => 'done' as const),
|
|
138
|
+
timeout,
|
|
139
|
+
]).catch((err: any) => {
|
|
140
|
+
cs.error = err?.message || String(err)
|
|
141
|
+
return 'failed' as const
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
cs.durationMs = Date.now() - cs.startTime
|
|
145
|
+
|
|
146
|
+
if (result === 'timeout') {
|
|
147
|
+
cs.status = 'timeout'
|
|
148
|
+
cs.error = `Exceeded ${cs.timeLimitMinutes}min + 30s safety margin`
|
|
149
|
+
pushActivity(cs, `[TIMEOUT: ${cs.timeLimitMinutes} min limit reached]`)
|
|
150
|
+
} else if (result === 'failed') {
|
|
151
|
+
cs.status = 'failed'
|
|
152
|
+
pushActivity(cs, `[FAILED: ${cs.error}]`)
|
|
153
|
+
} else {
|
|
154
|
+
// Check if LESSONS.md was actually written
|
|
155
|
+
if (fs.existsSync(container.paths.resolve(attemptFolder, 'LESSONS.md'))) {
|
|
156
|
+
cs.lessonsWritten = true
|
|
157
|
+
}
|
|
158
|
+
cs.status = 'done'
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function runBatch(
|
|
163
|
+
batch: ChallengeState[],
|
|
164
|
+
container: any,
|
|
165
|
+
sessionFolder: string,
|
|
166
|
+
abortSignal: { aborted: boolean },
|
|
167
|
+
): Promise<void> {
|
|
168
|
+
const results = await Promise.allSettled(
|
|
169
|
+
batch.map((cs) => runChallenge(cs, container, sessionFolder, abortSignal))
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
for (let i = 0; i < results.length; i++) {
|
|
173
|
+
const r = results[i]
|
|
174
|
+
if (r.status === 'rejected' && batch[i].status === 'running') {
|
|
175
|
+
batch[i].status = 'failed'
|
|
176
|
+
batch[i].error = String(r.reason)
|
|
177
|
+
batch[i].durationMs = Date.now() - batch[i].startTime
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function runSynthesis(
|
|
183
|
+
challenges: ChallengeState[],
|
|
184
|
+
container: any,
|
|
185
|
+
sessionFolder: string,
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
const fs = container.feature('fs')
|
|
188
|
+
const paths = container.paths
|
|
189
|
+
|
|
190
|
+
// Gather all LESSONS.md content
|
|
191
|
+
const lessonParts: string[] = []
|
|
192
|
+
const summaryParts: string[] = []
|
|
193
|
+
|
|
194
|
+
for (const cs of challenges) {
|
|
195
|
+
const lessonsPath = paths.resolve(cs.attemptFolder, 'LESSONS.md')
|
|
196
|
+
const statusLabel = cs.status === 'done' ? 'completed' : cs.status
|
|
197
|
+
const duration = formatElapsed(cs.durationMs)
|
|
198
|
+
|
|
199
|
+
summaryParts.push(`- **${cs.title}** (${cs.slug}): ${statusLabel} in ${duration}${cs.lessonsWritten ? '' : ' — no LESSONS.md'}`)
|
|
200
|
+
|
|
201
|
+
if (cs.lessonsWritten && fs.existsSync(lessonsPath)) {
|
|
202
|
+
const content = fs.readFile(lessonsPath) as string
|
|
203
|
+
lessonParts.push(`## ${cs.title} (${cs.slug})\n\n${content}`)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const done = challenges.filter(c => c.status === 'done').length
|
|
208
|
+
const failed = challenges.filter(c => c.status === 'failed' || c.status === 'timeout').length
|
|
209
|
+
|
|
210
|
+
const synthesisPrompt = `You are reviewing the results of a batch challenge evaluation session for the Luca framework.
|
|
211
|
+
|
|
212
|
+
${done} challenges completed successfully. ${failed} challenges failed or timed out.
|
|
213
|
+
|
|
214
|
+
## Challenge Results
|
|
215
|
+
|
|
216
|
+
${summaryParts.join('\n')}
|
|
217
|
+
|
|
218
|
+
## Individual LESSONS.md Files
|
|
219
|
+
|
|
220
|
+
${lessonParts.length > 0 ? lessonParts.join('\n\n---\n\n') : '(No LESSONS.md files were produced)'}
|
|
221
|
+
|
|
222
|
+
## Your Task
|
|
223
|
+
|
|
224
|
+
Write a RETRO.md file in the current directory that contains:
|
|
225
|
+
|
|
226
|
+
1. **What Went Well** — patterns and capabilities that worked reliably across challenges
|
|
227
|
+
2. **What Didn't Go Well** — common struggles, failures, and pain points
|
|
228
|
+
3. **Actionable Improvements** — specific, concrete steps to improve the CLAUDE.md, SKILL.md, framework docs, or luca internals that would help future challenge runs succeed faster and more reliably
|
|
229
|
+
4. **Challenge-by-Challenge Notes** — brief per-challenge observations worth preserving
|
|
230
|
+
|
|
231
|
+
Be specific and actionable. Reference concrete file paths, APIs, and patterns. This retro should directly inform what we work on next.`
|
|
232
|
+
|
|
233
|
+
// Write synthesis prompt to a temp file and run it through luca prompt
|
|
234
|
+
const synthPromptPath = paths.resolve(sessionFolder, '_synthesis-prompt.md')
|
|
235
|
+
fs.ensureFile(synthPromptPath, `---\nrepeatable: true\n---\n\n${synthesisPrompt}`, true)
|
|
236
|
+
|
|
237
|
+
fs.ensureFolder(paths.resolve(sessionFolder, 'logs'))
|
|
238
|
+
|
|
239
|
+
await container.proc.spawnAndCapture('luca', [
|
|
240
|
+
'prompt', 'claude', synthPromptPath,
|
|
241
|
+
'--in-folder', sessionFolder,
|
|
242
|
+
'--out-file', `${sessionFolder}/logs/synthesis-session.md`,
|
|
243
|
+
'--dont-touch-file',
|
|
244
|
+
'--preserve-frontmatter',
|
|
245
|
+
], {
|
|
246
|
+
onOutput: (str: string) => { process.stdout.write(str) },
|
|
247
|
+
onError: (str: string) => { process.stderr.write(str) },
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ─── Ink Dashboard ──────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
async function renderDashboard(
|
|
254
|
+
challenges: ChallengeState[],
|
|
255
|
+
container: any,
|
|
256
|
+
sessionFolder: string,
|
|
257
|
+
batchSize: number,
|
|
258
|
+
): Promise<boolean> {
|
|
259
|
+
const ink = container.feature('ink', { enable: true })
|
|
260
|
+
await ink.loadModules()
|
|
261
|
+
|
|
262
|
+
const React = ink.React
|
|
263
|
+
const h = React.createElement
|
|
264
|
+
const { Box, Text } = ink.components
|
|
265
|
+
const { useApp, useInput, useStdout } = ink.hooks
|
|
266
|
+
const { useState, useEffect } = React
|
|
267
|
+
|
|
268
|
+
const numBatches = Math.ceil(challenges.length / batchSize)
|
|
269
|
+
let currentBatchIndex = 0
|
|
270
|
+
let allBatchesDone = false
|
|
271
|
+
let userAborted = false
|
|
272
|
+
const abortSignal = { aborted: false }
|
|
273
|
+
|
|
274
|
+
// Run batches in sequence outside React
|
|
275
|
+
const orchestrate = async () => {
|
|
276
|
+
for (let b = 0; b < numBatches; b++) {
|
|
277
|
+
if (abortSignal.aborted) break
|
|
278
|
+
currentBatchIndex = b
|
|
279
|
+
const start = b * batchSize
|
|
280
|
+
const batch = challenges.slice(start, start + batchSize)
|
|
281
|
+
await runBatch(batch, container, sessionFolder, abortSignal)
|
|
282
|
+
}
|
|
283
|
+
allBatchesDone = true
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const orchestrationPromise = orchestrate().catch(() => { allBatchesDone = true })
|
|
287
|
+
|
|
288
|
+
function App() {
|
|
289
|
+
const { exit } = useApp()
|
|
290
|
+
const { stdout } = useStdout()
|
|
291
|
+
const [tick, setTick] = useState(0)
|
|
292
|
+
const [focusIdx, setFocusIdx] = useState(0)
|
|
293
|
+
|
|
294
|
+
const cols = stdout?.columns || 120
|
|
295
|
+
const rows = stdout?.rows || 40
|
|
296
|
+
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
const timer = setInterval(() => setTick((t: number) => t + 1), 250)
|
|
299
|
+
return () => clearInterval(timer)
|
|
300
|
+
}, [])
|
|
301
|
+
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
if (allBatchesDone) {
|
|
304
|
+
setTimeout(() => exit(), 600)
|
|
305
|
+
}
|
|
306
|
+
}, [tick])
|
|
307
|
+
|
|
308
|
+
useInput((input: string, key: any) => {
|
|
309
|
+
if (input === 'q' || (key.ctrl && input === 'c')) {
|
|
310
|
+
userAborted = true
|
|
311
|
+
abortSignal.aborted = true
|
|
312
|
+
// Kill all active child processes
|
|
313
|
+
for (const cp of activeChildProcesses) {
|
|
314
|
+
try { cp.kill?.('SIGTERM') } catch {}
|
|
315
|
+
}
|
|
316
|
+
activeChildProcesses.clear()
|
|
317
|
+
exit()
|
|
318
|
+
}
|
|
319
|
+
if (key.upArrow) setFocusIdx((i: number) => Math.max(0, i - 1))
|
|
320
|
+
if (key.downArrow) setFocusIdx((i: number) => Math.min(challenges.length - 1, i + 1))
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
const done = challenges.filter(c => c.status === 'done').length
|
|
324
|
+
const failed = challenges.filter(c => c.status === 'failed' || c.status === 'timeout').length
|
|
325
|
+
const running = challenges.filter(c => c.status === 'running' || c.status === 'bootstrapping').length
|
|
326
|
+
const queued = challenges.filter(c => c.status === 'queued').length
|
|
327
|
+
|
|
328
|
+
// Progress bar
|
|
329
|
+
const progress = challenges.length > 0 ? (done + failed) / challenges.length : 0
|
|
330
|
+
const barWidth = 20
|
|
331
|
+
const filled = Math.round(progress * barWidth)
|
|
332
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled)
|
|
333
|
+
|
|
334
|
+
// Spinner frame
|
|
335
|
+
const spinFrame = SPINNER[tick % SPINNER.length]
|
|
336
|
+
|
|
337
|
+
// Detail panel lines
|
|
338
|
+
const focused = challenges[focusIdx]
|
|
339
|
+
const detailHeight = Math.min(8, Math.max(rows - challenges.length - 8, 4))
|
|
340
|
+
const detailLines = focused ? focused.activityLines.slice(-detailHeight) : []
|
|
341
|
+
|
|
342
|
+
return h(Box, { flexDirection: 'column', width: cols },
|
|
343
|
+
// ── Header ──
|
|
344
|
+
h(Box, { paddingX: 1, marginBottom: 1, justifyContent: 'space-between' },
|
|
345
|
+
h(Text, { bold: true, color: '#61dafb' }, 'LUCA CHALLENGES'),
|
|
346
|
+
h(Text, null,
|
|
347
|
+
h(Text, { dimColor: true }, `Batch ${currentBatchIndex + 1}/${numBatches} `),
|
|
348
|
+
h(Text, { color: 'cyan' }, bar),
|
|
349
|
+
h(Text, { dimColor: true }, ` ${done + failed}/${challenges.length}`),
|
|
350
|
+
),
|
|
351
|
+
),
|
|
352
|
+
// ── Stats row ──
|
|
353
|
+
h(Box, { paddingX: 1, marginBottom: 1, gap: 2 },
|
|
354
|
+
h(Text, { color: 'green' }, `${done} done`),
|
|
355
|
+
h(Text, { color: 'red' }, `${failed} failed`),
|
|
356
|
+
running > 0
|
|
357
|
+
? h(Text, { color: 'cyan' }, `${running} running`)
|
|
358
|
+
: null,
|
|
359
|
+
queued > 0
|
|
360
|
+
? h(Text, { dimColor: true }, `${queued} queued`)
|
|
361
|
+
: null,
|
|
362
|
+
),
|
|
363
|
+
// ── Challenge rows ──
|
|
364
|
+
...challenges.map((cs, i) => {
|
|
365
|
+
const isFocused = i === focusIdx
|
|
366
|
+
const elapsed = cs.status === 'queued'
|
|
367
|
+
? '--:--'
|
|
368
|
+
: cs.status === 'done' || cs.status === 'failed' || cs.status === 'timeout'
|
|
369
|
+
? formatElapsed(cs.durationMs)
|
|
370
|
+
: formatElapsed(Date.now() - cs.startTime)
|
|
371
|
+
|
|
372
|
+
let icon = ' · '
|
|
373
|
+
let iconColor = 'gray'
|
|
374
|
+
if (cs.status === 'bootstrapping') { icon = ' ⚙ '; iconColor = 'yellow' }
|
|
375
|
+
else if (cs.status === 'running') { icon = ` ${spinFrame} `; iconColor = 'cyan' }
|
|
376
|
+
else if (cs.status === 'done') { icon = ' ✓ '; iconColor = 'green' }
|
|
377
|
+
else if (cs.status === 'failed') { icon = ' ✗ '; iconColor = 'red' }
|
|
378
|
+
else if (cs.status === 'timeout') { icon = ' ⏱ '; iconColor = 'yellow' }
|
|
379
|
+
|
|
380
|
+
const slugDisplay = cs.slug.slice(0, 36).padEnd(36)
|
|
381
|
+
const elapsedDisplay = elapsed.padStart(6)
|
|
382
|
+
const activityWidth = Math.max(0, cols - 52)
|
|
383
|
+
const activity = cs.lastActivity ? cs.lastActivity.slice(0, activityWidth) : ''
|
|
384
|
+
const lessonsTag = cs.lessonsWritten ? ' [L]' : ''
|
|
385
|
+
|
|
386
|
+
return h(Box, { key: cs.id, paddingX: 1 },
|
|
387
|
+
h(Text, { color: isFocused ? 'white' : undefined, bold: isFocused, inverse: isFocused },
|
|
388
|
+
h(Text, { color: iconColor }, icon),
|
|
389
|
+
h(Text, null, slugDisplay),
|
|
390
|
+
h(Text, { dimColor: !isFocused }, ` ${elapsedDisplay} `),
|
|
391
|
+
h(Text, { dimColor: true }, activity),
|
|
392
|
+
cs.lessonsWritten
|
|
393
|
+
? h(Text, { color: 'green', bold: true }, lessonsTag)
|
|
394
|
+
: null,
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
}),
|
|
398
|
+
// ── Detail panel ──
|
|
399
|
+
h(Box, {
|
|
400
|
+
flexDirection: 'column',
|
|
401
|
+
borderStyle: 'round',
|
|
402
|
+
borderColor: focused?.status === 'running' ? 'cyan'
|
|
403
|
+
: focused?.status === 'done' ? 'green'
|
|
404
|
+
: focused?.status === 'failed' || focused?.status === 'timeout' ? 'red'
|
|
405
|
+
: 'gray',
|
|
406
|
+
paddingX: 1,
|
|
407
|
+
marginTop: 1,
|
|
408
|
+
marginX: 1,
|
|
409
|
+
height: detailHeight + 2,
|
|
410
|
+
},
|
|
411
|
+
h(Box, { justifyContent: 'space-between' },
|
|
412
|
+
h(Text, { bold: true }, focused ? focused.title : ''),
|
|
413
|
+
focused && focused.status !== 'queued'
|
|
414
|
+
? h(Text, { dimColor: true },
|
|
415
|
+
focused.status === 'running' || focused.status === 'bootstrapping'
|
|
416
|
+
? formatElapsed(Date.now() - focused.startTime)
|
|
417
|
+
: formatElapsed(focused.durationMs),
|
|
418
|
+
)
|
|
419
|
+
: null,
|
|
420
|
+
),
|
|
421
|
+
h(Text, { wrap: 'truncate', dimColor: true },
|
|
422
|
+
detailLines.length > 0 ? detailLines.join('\n') : '(waiting...)',
|
|
423
|
+
),
|
|
424
|
+
),
|
|
425
|
+
// ── Footer ──
|
|
426
|
+
h(Box, { paddingX: 1, marginTop: 1, gap: 3 },
|
|
427
|
+
h(Text, { dimColor: true }, '↑↓ navigate'),
|
|
428
|
+
h(Text, { dimColor: true }, 'q quit'),
|
|
429
|
+
),
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
await ink.render(h(App))
|
|
434
|
+
await ink.waitUntilExit()
|
|
435
|
+
|
|
436
|
+
if (userAborted) return false
|
|
437
|
+
|
|
438
|
+
await orchestrationPromise
|
|
439
|
+
return true
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ─── Main Handler ───────────────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
export async function tryAllChallenges(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
445
|
+
const container = context.container as any
|
|
446
|
+
const fs = container.feature('fs')
|
|
447
|
+
const paths = container.paths
|
|
448
|
+
const batchSize = options['batch-size']
|
|
449
|
+
|
|
450
|
+
await container.docs.load()
|
|
451
|
+
const allChallenges = await container.docs.queries.challenges.fetchAll()
|
|
452
|
+
|
|
453
|
+
if (allChallenges.length === 0) {
|
|
454
|
+
container.ui.print('No challenges found in docs/challenges/')
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Build challenge states
|
|
459
|
+
const numBatches = Math.ceil(allChallenges.length / batchSize)
|
|
460
|
+
const challengeStates: ChallengeState[] = allChallenges.map((c: any, i: number) => ({
|
|
461
|
+
id: c.id,
|
|
462
|
+
slug: c.id.split('/').pop()!,
|
|
463
|
+
title: c.title || c.id.split('/').pop()!,
|
|
464
|
+
status: 'queued' as ChallengeStatus,
|
|
465
|
+
startTime: 0,
|
|
466
|
+
durationMs: 0,
|
|
467
|
+
timeLimitMinutes: options['time-limit'] ?? c.meta?.maxTime ?? 5,
|
|
468
|
+
lastActivity: '',
|
|
469
|
+
activityLines: [],
|
|
470
|
+
lessonsWritten: false,
|
|
471
|
+
attemptFolder: '',
|
|
472
|
+
error: undefined,
|
|
473
|
+
batchIndex: Math.floor(i / batchSize),
|
|
474
|
+
}))
|
|
475
|
+
|
|
476
|
+
// Dry run — just print the schedule
|
|
477
|
+
if (options['dry-run']) {
|
|
478
|
+
container.ui.print(`\n${allChallenges.length} challenges in ${numBatches} batches of ${batchSize}:\n`)
|
|
479
|
+
for (let b = 0; b < numBatches; b++) {
|
|
480
|
+
const batch = challengeStates.filter(c => c.batchIndex === b)
|
|
481
|
+
container.ui.print(` Batch ${b + 1}:`)
|
|
482
|
+
for (const cs of batch) {
|
|
483
|
+
container.ui.print(` - ${cs.slug} (${cs.timeLimitMinutes}min)`)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Create session folder
|
|
490
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
491
|
+
const sessionFolder = paths.resolve(`attempts/session-${timestamp}`)
|
|
492
|
+
fs.ensureFolder(sessionFolder)
|
|
493
|
+
fs.ensureFolder(`${sessionFolder}/logs`)
|
|
494
|
+
|
|
495
|
+
container.ui.print(`Session: ${sessionFolder}`)
|
|
496
|
+
container.ui.print(`${allChallenges.length} challenges, ${numBatches} batches of ${batchSize}\n`)
|
|
497
|
+
|
|
498
|
+
// Run dashboard
|
|
499
|
+
const completed = await renderDashboard(challengeStates, container, sessionFolder, batchSize)
|
|
500
|
+
|
|
501
|
+
if (!completed) {
|
|
502
|
+
container.ui.print('\nAborted by user.')
|
|
503
|
+
process.exit(1)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Print summary
|
|
507
|
+
const done = challengeStates.filter(c => c.status === 'done').length
|
|
508
|
+
const failed = challengeStates.filter(c => c.status === 'failed' || c.status === 'timeout').length
|
|
509
|
+
const withLessons = challengeStates.filter(c => c.lessonsWritten).length
|
|
510
|
+
|
|
511
|
+
container.ui.print(`\n${'─'.repeat(60)}`)
|
|
512
|
+
container.ui.print(`Results: ${done} done, ${failed} failed, ${withLessons} with LESSONS.md`)
|
|
513
|
+
container.ui.print(`Session folder: ${sessionFolder}`)
|
|
514
|
+
|
|
515
|
+
// Write a session manifest
|
|
516
|
+
const manifest = challengeStates.map(cs => ({
|
|
517
|
+
slug: cs.slug,
|
|
518
|
+
status: cs.status,
|
|
519
|
+
durationMs: cs.durationMs,
|
|
520
|
+
lessonsWritten: cs.lessonsWritten,
|
|
521
|
+
error: cs.error,
|
|
522
|
+
}))
|
|
523
|
+
fs.ensureFile(
|
|
524
|
+
paths.resolve(sessionFolder, 'manifest.json'),
|
|
525
|
+
JSON.stringify(manifest, null, 2),
|
|
526
|
+
true,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
// Synthesis
|
|
530
|
+
if (withLessons > 0) {
|
|
531
|
+
container.ui.print(`\nRunning synthesis across ${withLessons} LESSONS.md files...\n`)
|
|
532
|
+
await runSynthesis(challengeStates, container, sessionFolder)
|
|
533
|
+
container.ui.print(`\nRetro written to ${sessionFolder}/RETRO.md`)
|
|
534
|
+
} else {
|
|
535
|
+
container.ui.print('\nNo LESSONS.md files produced — skipping synthesis.')
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export default {
|
|
540
|
+
description: 'Run all challenges in parallel batches with a live dashboard, then synthesize lessons into a retro.',
|
|
541
|
+
argsSchema,
|
|
542
|
+
handler: tryAllChallenges,
|
|
543
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { commands, CommandOptionsSchema } from '@soederpop/luca'
|
|
3
|
+
import type { ContainerContext } from '@soederpop/luca'
|
|
4
|
+
|
|
5
|
+
declare module '@soederpop/luca' {
|
|
6
|
+
interface AvailableCommands {
|
|
7
|
+
tryChallenge: ReturnType<typeof commands.registerHandler>
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const argsSchema = CommandOptionsSchema.extend({
|
|
12
|
+
'time-limit': z.number().optional().describe('Time limit in minutes (defaults to challenge maxTime, then 5)'),
|
|
13
|
+
list: z.boolean().default(false).describe('List available challenges')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export async function tryChallenge(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
17
|
+
const container = context.container as any
|
|
18
|
+
const fs = container.feature('fs')
|
|
19
|
+
let requestedChallengeId = options._[1]!
|
|
20
|
+
|
|
21
|
+
await container.docs.load()
|
|
22
|
+
|
|
23
|
+
const challenges = await container.docs.queries.challenges.fetchAll()
|
|
24
|
+
|
|
25
|
+
if (!requestedChallengeId) {
|
|
26
|
+
container.ui.print('Available challenges:')
|
|
27
|
+
challenges.forEach(c => container.ui.print(`- ${c.id.split('/').pop()}`))
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
requestedChallengeId = requestedChallengeId.startsWith('challenges/') ? requestedChallengeId : `challenges/${requestedChallengeId}`
|
|
32
|
+
|
|
33
|
+
const challenge = challenges.find(c => c.id === requestedChallengeId || c.id.split('/').pop() === requestedChallengeId)
|
|
34
|
+
|
|
35
|
+
// Derive slug from the challenge id (e.g. "challenges/build-an-api" -> "build-an-api")
|
|
36
|
+
const slug = challenge.id.split('/').pop()
|
|
37
|
+
|
|
38
|
+
// Determine attempt number by counting existing attempts for this challenge
|
|
39
|
+
fs.ensureFolder('attempts')
|
|
40
|
+
const existing = fs.existsSync('attempts')
|
|
41
|
+
? (await fs.readdir('attempts') as string[]).filter((name: string) => name.startsWith(slug + '-attempt-'))
|
|
42
|
+
: []
|
|
43
|
+
const attemptNumber = existing.length + 1
|
|
44
|
+
const attemptFolder = `attempts/${slug}-attempt-${attemptNumber}`
|
|
45
|
+
|
|
46
|
+
fs.ensureFolder(attemptFolder)
|
|
47
|
+
|
|
48
|
+
const timeLimitMinutes = options['time-limit'] ?? challenge.meta?.maxTime ?? 5
|
|
49
|
+
const timeLimitMs = timeLimitMinutes * 60 * 1000
|
|
50
|
+
|
|
51
|
+
container.ui.print(`Running Challenge: ${challenge.title}`)
|
|
52
|
+
container.ui.print(`Attempt #${attemptNumber} in ${attemptFolder}`)
|
|
53
|
+
container.ui.print(`Time limit: ${timeLimitMinutes} minutes`)
|
|
54
|
+
|
|
55
|
+
// Bootstrap the attempt folder
|
|
56
|
+
await container.proc.spawnAndCapture('luca', ['bootstrap'], {
|
|
57
|
+
cwd: container.paths.resolve(attemptFolder),
|
|
58
|
+
onOutput: (str) => { console.log(str) },
|
|
59
|
+
onError: (str) => { console.error(str) },
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const promptCommandArgs = [
|
|
63
|
+
'prompt', 'claude', `docs/${challenge.id}`,
|
|
64
|
+
'--exclude-sections', 'Internal Notes',
|
|
65
|
+
'--out-file', `docs/sessions/${challenge.id.split('/').pop()}/attempt-log-${attemptNumber}.md`,
|
|
66
|
+
'--in-folder', attemptFolder, '--dont-touch-file'
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
const promptProcess = container.proc.spawnAndCapture('luca', promptCommandArgs, {
|
|
70
|
+
onOutput: (str) => {
|
|
71
|
+
console.log(str)
|
|
72
|
+
},
|
|
73
|
+
onError: (str) => {
|
|
74
|
+
console.error(str)
|
|
75
|
+
},
|
|
76
|
+
onExit: () => {
|
|
77
|
+
console.log('Claude Exited')
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
process.exit(0)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
reject(new Error(`Time limit of ${timeLimitMinutes} minutes reached`))
|
|
86
|
+
}, timeLimitMs)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await Promise.race([promptProcess, timeout])
|
|
91
|
+
} catch (err: any) {
|
|
92
|
+
container.ui.print(err.message)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default {
|
|
97
|
+
description: 'Try running one of the evaluation challenges.',
|
|
98
|
+
argsSchema,
|
|
99
|
+
handler: tryChallenge,
|
|
100
|
+
}
|