cleanup
This commit is contained in:
parent
dbde587ee9
commit
48237c8d85
|
|
@ -1,247 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { DEFAULTS, USAGE, loadConfig } from '../src/config.mjs'
|
||||
import { Orchestrator } from '../src/orchestrator.mjs'
|
||||
import { RunController } from '../src/controller.mjs'
|
||||
import { startServer, runsBaseDir } from '../src/server.mjs'
|
||||
import { installTerminalControls } from '../src/terminal.mjs'
|
||||
import { printLog } from '../src/console.mjs'
|
||||
import { listRuns, loadRun } from '../src/state.mjs'
|
||||
import { dashboardUrls } from '../src/net.mjs'
|
||||
import { guardFeedbackText } from '../src/guard.mjs'
|
||||
import { loadGoal } from '../src/goal.mjs'
|
||||
import { listGoalSummaries } from '../src/goals.mjs'
|
||||
import { confirmStart, renderPreflight } from '../src/preflight.mjs'
|
||||
import { runReportCommand } from '../src/reportcli.mjs'
|
||||
import { c } from '../src/ansi.mjs'
|
||||
|
||||
/** Exit codes by final run status (documented in the README). */
|
||||
export const EXIT_CODES = {
|
||||
done: 0,
|
||||
'done-with-errors': 1,
|
||||
aborted: 2,
|
||||
'stopped-budget': 3,
|
||||
stalled: 4,
|
||||
'stopped-guard': 5,
|
||||
}
|
||||
|
||||
function dashboardLink(url, note, lan = []) {
|
||||
process.stdout.write(`\n ${c.gray('Dashboard')} ${url} ${c.gray('(Ctrl+Click to open)')}\n`)
|
||||
for (const u of lan) {
|
||||
if (u !== url) process.stdout.write(` ${c.gray('On this network (e.g. phone)')} ${c.bold(u)}\n`)
|
||||
}
|
||||
if (note) process.stdout.write(` ${c.gray(note)}\n`)
|
||||
}
|
||||
|
||||
/** Resolve after `ms`, without keeping the process alive just for the timer. */
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => {
|
||||
const t = setTimeout(resolve, ms)
|
||||
t.unref?.()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle SIGINT/SIGTERM so Ctrl+C always stops the loop and exits — even when a
|
||||
* dashboard tab holds an SSE connection open. A second signal exits immediately,
|
||||
* and a hard guard force-exits if graceful cleanup misbehaves.
|
||||
*/
|
||||
function installSignalHandlers(onSignal, exitCode = 130) {
|
||||
let signals = 0
|
||||
const handler = async () => {
|
||||
signals += 1
|
||||
if (signals > 1) process.exit(exitCode)
|
||||
const guard = setTimeout(() => process.exit(exitCode), 8000)
|
||||
guard.unref?.()
|
||||
try {
|
||||
await onSignal()
|
||||
} catch {
|
||||
/* ignore — we are exiting anyway */
|
||||
}
|
||||
clearTimeout(guard)
|
||||
process.exit(exitCode)
|
||||
}
|
||||
process.on('SIGINT', handler)
|
||||
process.on('SIGTERM', handler)
|
||||
}
|
||||
|
||||
function printGoals(project) {
|
||||
const goals = listGoalSummaries(project)
|
||||
if (!goals.length) {
|
||||
process.stdout.write(
|
||||
`\n No goals found. Add JSON/Markdown goals under .kilocode-loop/goals/, or a markdown plan with "- [ ]" items under plans/ or .kilo/plans/.\n\n`,
|
||||
)
|
||||
return
|
||||
}
|
||||
process.stdout.write(`\n ${c.bold('Goals / plans')} ${c.gray(project)}\n\n`)
|
||||
for (const g of goals) {
|
||||
const last = g.lastRun ? `${g.lastRun.status} · ${g.lastRun.startedAt}` : 'never run'
|
||||
const verify = g.globalVerify ? ` · global verify: ${g.globalVerify}` : ''
|
||||
process.stdout.write(` ${String(g.percent).padStart(3)}% ${String(g.done)}/${g.total} ${c.bold(g.file)}\n`)
|
||||
process.stdout.write(` ${g.title}${c.gray(verify)}\n`)
|
||||
process.stdout.write(` ${c.gray(`last run: ${last}`)}\n\n`)
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// `kilo-loop report` is a read-only subcommand: it must not load a goal or
|
||||
// start anything, so handle it before normal config parsing.
|
||||
const argv = process.argv.slice(2)
|
||||
if (argv[0] === 'report') {
|
||||
process.exit(runReportCommand(argv.slice(1)))
|
||||
}
|
||||
|
||||
let config
|
||||
try {
|
||||
config = loadConfig(argv)
|
||||
} catch (err) {
|
||||
process.stderr.write(`\n Error: ${err.message}\n\n${USAGE}\n`)
|
||||
process.exit(2)
|
||||
}
|
||||
if (config.help) {
|
||||
process.stdout.write(USAGE + '\n')
|
||||
process.exit(0)
|
||||
}
|
||||
if (config.listGoals) {
|
||||
printGoals(config.project)
|
||||
return
|
||||
}
|
||||
|
||||
if (config.promptExtra && fs.existsSync(config.promptExtra)) {
|
||||
config.promptExtra = fs.readFileSync(config.promptExtra, 'utf8')
|
||||
}
|
||||
|
||||
// Launcher mode: no goal on the CLI. Serve the dashboard so a plan can be
|
||||
// picked (and its verify configured) in the UI.
|
||||
if (config.launcher) {
|
||||
const controller = new RunController(config)
|
||||
const dashboard = await startServer(config, controller)
|
||||
dashboardLink(dashboard.url, 'No goal given — pick a plan in the dashboard.', dashboard.urls?.lan || [])
|
||||
installSignalHandlers(async () => {
|
||||
if (controller.isActive()) {
|
||||
controller.requestStop()
|
||||
// Give the current session a moment to die so no child is orphaned.
|
||||
await Promise.race([controller.whenRunSettles(), delay(5000)])
|
||||
}
|
||||
await dashboard.close().catch(() => {})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve the goal up front so the pre-flight shows exactly what will run and
|
||||
// a broken goal file fails before anything is started.
|
||||
let goal
|
||||
try {
|
||||
goal = loadGoal(config)
|
||||
} catch (err) {
|
||||
process.stderr.write(`\n Error: ${err.message}\n`)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
// --resume: load the previous run before the pre-flight so stages it already
|
||||
// completed are shown (and treated) as done instead of being repeated. The
|
||||
// agent may report a variant id (e.g. "phase3-resource-and-policy-cleanup"),
|
||||
// so match by equality or prefix against the goal's stage ids.
|
||||
let prevRun = null
|
||||
const resumedStageIds = new Set()
|
||||
if (config.resume === 'last' || config.resume === 'latest') {
|
||||
const [latest] = listRuns(runsBaseDir(config))
|
||||
if (latest?.runId) {
|
||||
config.resume = latest.runId
|
||||
} else {
|
||||
process.stderr.write(' Warning: --resume last requested but no saved runs were found — starting fresh.\n')
|
||||
config.resume = ''
|
||||
}
|
||||
}
|
||||
if (config.resume) {
|
||||
try {
|
||||
prevRun = loadRun(runsBaseDir(config), config.resume)
|
||||
const reported = new Set(
|
||||
(prevRun.iterations || []).filter((it) => it.stageStatus === 'done' && it.stageId).map((it) => it.stageId),
|
||||
)
|
||||
for (const s of goal.stages || []) {
|
||||
if ([...reported].some((d) => d === s.id || d.startsWith(s.id))) {
|
||||
s.done = true
|
||||
resumedStageIds.add(s.id)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
process.stderr.write(` Warning: could not load run "${config.resume}" — starting fresh.\n`)
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(renderPreflight(config, goal, dashboardUrls(config).primary))
|
||||
if (!(await confirmStart(config))) {
|
||||
process.stdout.write(`\n ${c.yellow('Cancelled')} — nothing was started. Edit the goal or config, then run again.\n`)
|
||||
return
|
||||
}
|
||||
|
||||
const orchestrator = new Orchestrator(config)
|
||||
orchestrator.state.on('log', (entry) => printLog(entry, { quiet: config.quiet }))
|
||||
|
||||
if (prevRun) {
|
||||
// Only reuse the previous session when shared context is on; fresh runs
|
||||
// start a new session every iteration regardless of --resume.
|
||||
if (prevRun.sessionID && config.sharedContext) orchestrator.state.update({ sessionID: prevRun.sessionID })
|
||||
for (const id of resumedStageIds) orchestrator.completedStages.add(id)
|
||||
const last = (prevRun.iterations || []).slice(-1)[0]
|
||||
if (last?.percent != null) orchestrator.lastPercent = last.percent
|
||||
// If the previous run was interrupted by the safety guard, carry that note
|
||||
// into this run's first iteration so the agent does not retry the command.
|
||||
const blocked = [...(prevRun.iterations || [])].reverse().find((it) => it.guardViolation)
|
||||
if (blocked) {
|
||||
orchestrator.guardFeedback = guardFeedbackText(blocked.guardViolation)
|
||||
orchestrator.log('warn', `Previous run hit the safety guard (${blocked.guardViolation.id}) — the first iteration carries the safety feedback.`)
|
||||
}
|
||||
orchestrator.log('system', `Resumed run ${config.resume} (session ${prevRun.sessionID || '—'}, last ${orchestrator.lastPercent}%).`)
|
||||
}
|
||||
|
||||
const controller = new RunController(config)
|
||||
controller.attach(orchestrator)
|
||||
const dashboard = await startServer(config, controller)
|
||||
const controls = installTerminalControls(orchestrator)
|
||||
|
||||
dashboardLink(dashboard.url, null, dashboard.urls?.lan || [])
|
||||
|
||||
let exitCode = 0
|
||||
const shutdown = async (code) => {
|
||||
controls.dispose()
|
||||
// Hard guard: never hang on exit even if close() misbehaves.
|
||||
const guard = setTimeout(() => process.exit(code), 3000)
|
||||
guard.unref?.()
|
||||
await dashboard.close().catch(() => {})
|
||||
clearTimeout(guard)
|
||||
process.exit(code)
|
||||
}
|
||||
|
||||
const runPromise = orchestrator.run()
|
||||
installSignalHandlers(async () => {
|
||||
orchestrator.requestStop()
|
||||
await Promise.race([runPromise.catch(() => {}), delay(8000)])
|
||||
})
|
||||
|
||||
try {
|
||||
const final = await runPromise
|
||||
exitCode = EXIT_CODES[final.status] ?? 1
|
||||
// Continuation aid: a run that did not reach `done` can be resumed with the
|
||||
// same goal, carrying its stage progress and (if any) safety feedback.
|
||||
if (final.status !== 'done' && final.runId) {
|
||||
const resume = config.goal
|
||||
? `kilo-loop --goal ${config.goal} --resume ${final.runId}`
|
||||
: `kilo-loop --resume ${final.runId} (pass the same --goal / --goal-text)`
|
||||
process.stdout.write(`\n ${c.yellow('Resume this run')}: ${resume}\n`)
|
||||
}
|
||||
} catch (err) {
|
||||
process.stderr.write(`\n Fatal: ${err?.stack || err}\n`)
|
||||
exitCode = 1
|
||||
}
|
||||
|
||||
if (config.keepOpen) {
|
||||
process.stdout.write(`\n Dashboard still open: ${dashboard.url} (Ctrl+Click to open, Ctrl+C to exit)\n`)
|
||||
return
|
||||
}
|
||||
await shutdown(exitCode)
|
||||
}
|
||||
|
||||
main()
|
||||
Loading…
Reference in a new issue