/self-drive: hand Claude a task and walk away; it decides alone, logs every call it would have asked you, holds risky actions, and keeps going

Experimental. Starts autonomous continuation only when invoked. Decisions and held actions are journaled. Recognized risky commands and messaging tools are held. Deadlines gate new tool calls; they cannot terminate running commands or agents. After a limit, stop releases the gate. Pattern checks are not a security sandbox.
claude plugin install self-drive --marketplace sruthik27/claude-foundry
Requires Claude Code 2.1.296+. Reload plugins or start a new session after installation.
/self-drive 30m finish the test suite
/self-drive
/self-drive stop
MIT licensed. Maintained by Sruthik Issac.
hooks/register.tsx 307 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Drive, Entry, EntryKind } from '../types'
5import { CONTINUE, MAX_CONTINUES, MAX_IDLE, brief, busyAgents, ended, fmtDur, logged, parseArgs, report, riskOf, riskyTool } from './drive.js'
6
7const PANE = 'self-drive'
8const drive = atom({ plugin: 'self-drive', key: 'drive' } as const, null)
9const LOG_TOOL = 'mcp__self-drive__self_drive_log'
10
11const KIND_STYLE: Record<EntryKind, { icon: string; color: string }> = {
12 decision: { icon: '◆', color: '#38bdf8' },
13 'needs-you': { icon: '✋', color: '#fbbf24' },
14 held: { icon: '⛔', color: '#f87171' },
15 note: { icon: '·', color: '#a3a3a3' },
16 finished: { icon: '✔', color: '#34d399' },
17}
18
19export const register: Register = on => {
20 /** Tool calls and log entries in the turn now running; drives the idle counter. */
21 let activity = 0
22 let briefTimer: { cancel: () => void } | undefined
23
24 on('session.start', async ($, e, next) => {
25 const r = await next(e)
26 await $.command.register({
27 name: 'self-drive',
28 description: 'Hand over a task and walk away: /self-drive [3h] <task> · /self-drive (dashboard) · /self-drive stop',
29 argumentHint: '[duration] <task> | stop',
30 })
31 await $.tool.register({
32 name: 'self_drive_log',
33 description: 'Self-drive journal. Record a decision you made on the user\'s behalf (kind "decision": title, alternatives, why), something only the user can do (kind "needs-you"), a milestone (kind "note"), or that all work is done (kind "finished": detail = summary). Only use while self-drive mode is on.',
34 inputSchema: {
35 type: 'object',
36 properties: {
37 kind: { type: 'string', enum: ['decision', 'needs-you', 'note', 'finished'] },
38 title: { type: 'string', description: 'One line: what was decided / what the user must do / the milestone.' },
39 alternatives: { type: 'string', description: 'decision: the options you did not pick.' },
40 why: { type: 'string', description: 'decision: why this one.' },
41 detail: { type: 'string', description: 'Anything else; for finished, the summary.' },
42 },
43 required: ['kind', 'title'],
44 },
45 })
46 // Restore an interrupted drive on resume.
47 const saved = (await $.store.get(`drive:${await $.session.id()}`)) as Drive | undefined
48 if (saved && (await read($, drive)) === null) {
49 await update($, drive, () => saved.status === 'driving' ? ended(saved, 'stopped', Date.now(), 'paused by restart') : saved)
50 if (saved.status === 'driving') $.ui.status('▶ self-drive paused by restart · /self-drive to view')
51 }
52 return r
53 })
54
55 on('classic.SessionStart', async ($, e, next) => {
56 const r = await next(e)
57 if (!['clear', 'resume', 'fork'].includes(e.source)) return r
58 activity = 0
59 briefTimer?.cancel(); briefTimer = undefined
60 const saved = e.source === 'clear' ? undefined : await $.store.get(`drive:${await $.session.id()}`) as Drive | undefined
61 await update($, drive, () => saved?.status === 'driving' ? ended(saved, 'stopped', Date.now(), 'paused by session switch') : saved ?? null)
62 $.ui.status(undefined)
63 return r
64 })
65
66 // ---- the command ----
67 on('command.run', { command: 'self-drive' }, async ($, e) => {
68 const args = e.args.trim()
69 const now = await $.clock.now()
70 const cur = await read($, drive)
71
72 if (args === '' ) {
73 if (!cur) return { text: 'Usage: /self-drive [30m|3h] <task>. Then walk away.' }
74 await $.ui.open({ id: PANE, title: 'Self-drive', focus: true })
75 return {}
76 }
77 if (args === 'stop') {
78 briefTimer?.cancel(); briefTimer = undefined
79 if (!cur || !['driving', 'limit'].includes(cur.status)) return { text: 'Self-drive is not running.' }
80 const d = await update($, drive, x => ended(x, 'stopped', now, 'stopped by you'))
81 if (d) await persist($, d, now)
82 $.ui.status(undefined)
83 return { text: 'Self-drive stopped. The dashboard keeps the log: /self-drive' }
84 }
85
86 // A slash command goes to command.run, never prompt.submit, so the drive starts here.
87 const { ms, task } = parseArgs(args)
88 if (!task) return { text: 'Give it a task: /self-drive 2h finish the upload feature' }
89 const d: Drive = {
90 task, startedAt: now, deadline: ms ? now + ms : undefined, status: 'driving',
91 entries: [], continues: 0, idle: 0,
92 }
93 await update($, drive, () => d)
94 await persist($, d, now)
95 activity = 0
96 void $.ui.open({ id: PANE, title: 'Self-drive' })
97 $.ui.status(`▶ self-drive on${d.deadline ? ` · until ${new Date(d.deadline).toTimeString().slice(0, 5)}` : ''}`)
98 // A command.run hook may not submit (the prompt would wait on the turn this hook holds),
99 // so the brief goes out on a timer, once the command has finished.
100 const text = brief(task, d.deadline, now)
101 briefTimer?.cancel()
102 briefTimer = $.clock.after(50, () => { briefTimer = undefined; void $.prompt.submit({ text, asUser: true }) })
103 return { text: `▶ Self-drive started${d.deadline ? ` for ${fmtDur(d.deadline - now)}` : ''}: ${task}` }
104 })
105
106 // ---- the journal tool ----
107 on('tool.call', { tool: LOG_TOOL }, async ($, e) => {
108 const input = (e as unknown as { input?: Record<string, string> }).input ?? (e as unknown as Record<string, string>)
109 const cur = await read($, drive)
110 if (!cur || cur.status !== 'driving') return { result: 'Not logged: self-drive is not on.' }
111 const now = await $.clock.now()
112 const kind = (['decision', 'needs-you', 'note', 'finished'].includes(input.kind ?? '') ? input.kind : 'note') as EntryKind
113 const entry: Entry = { at: now, kind, title: String(input.title ?? '').slice(0, 200) }
114 if (input.alternatives) entry.alternatives = String(input.alternatives).slice(0, 400)
115 if (input.why) entry.why = String(input.why).slice(0, 400)
116 if (input.detail) entry.detail = String(input.detail).slice(0, 4000)
117 activity += 1
118 const expired = cur.deadline !== undefined && now >= cur.deadline
119 const d = await update($, drive, x => x && {
120 ...x, entries: [...x.entries, entry],
121 ...(kind === 'finished' ? { status: expired ? 'limit' as const : 'finished' as const, endedAt: now, endReason: expired ? 'deadline reached' : 'work complete' } : {}),
122 })
123 if (d) await persist($, d, now)
124 if (kind === 'finished') $.ui.status('✔ self-drive finished · /self-drive for the report')
125 return { result: `Logged (${kind}).` }
126 })
127
128 // ---- guards while driving ----
129 on('tool.call', { tool: ['AskUserQuestion', 'ExitPlanMode', 'EnterPlanMode'] }, async ($, e, next) => {
130 const cur = await read($, drive)
131 if (cur?.status !== 'driving') return next(e)
132 return { deny: 'Self-drive is on: the user is away and cannot answer. Decide it yourself, record the choice with self_drive_log (kind "decision"), and keep going.' }
133 })
134
135 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
136 const cur = await read($, drive)
137 if (await deadlineBlocks($, cur)) return { deny: 'Self-drive budget reached. Stop work and write the final report. Use /self-drive stop to release this gate.' }
138 if (cur?.status !== 'driving') return next(e)
139 activity += 1
140 const why = riskOf(e.command)
141 if (!why) {
142 void update($, drive, x => x && { ...x, lastActivity: `$ ${e.command.split('\n')[0]?.slice(0, 80)}` })
143 return next(e)
144 }
145 const now = await $.clock.now()
146 const d = await update($, drive, x => logged(x, { at: now, kind: 'held', title: why, detail: '`' + e.command.slice(0, 300) + '`' }))
147 if (d) await persist($, d, now)
148 return { deny: `Held by self-drive (${why}): this is risky or outward-facing and the user is away. It has been added to the user's needs-you list automatically. Continue with everything else.` }
149 })
150
151 on('tool.call', async ($, e, next) => {
152 const cur = await read($, drive)
153 if (e.tool !== LOG_TOOL && await deadlineBlocks($, cur)) return { deny: 'Self-drive budget reached. Stop work and write the final report. Use /self-drive stop to release this gate.' }
154 if (cur?.status !== 'driving' || e.tool === LOG_TOOL || e.tool === 'Bash') return next(e)
155 activity += 1
156 const why = riskyTool(e.tool)
157 if (!why) {
158 const t = e.tool
159 void update($, drive, x => x && { ...x, lastActivity: t })
160 return next(e)
161 }
162 const now = await $.clock.now()
163 const d = await update($, drive, x => logged(x, { at: now, kind: 'held', title: why, detail: String(e.tool) }))
164 if (d) await persist($, d, now)
165 return { deny: `Held by self-drive (${why}): the user is away. Added to their needs-you list; continue with everything else.` }
166 })
167
168 // ---- keep going ----
169 on('turn.complete', async ($, e, next) => {
170 const r = await next(e)
171 if (e.agentId) return r
172 const cur = await read($, drive)
173 if (!cur || cur.status !== 'driving') return r
174 const now = await $.clock.now()
175 if (e.isAborted) {
176 // The person pressed Esc: they are back. Stop driving, keep the log.
177 const d = await update($, drive, x => ended(x, 'stopped', now, 'interrupted by you'))
178 if (d) await persist($, d, now)
179 $.ui.status(undefined)
180 return r
181 }
182 // Background agents still running: their completion wakes the main loop with a
183 // notification turn, so don't count this turn as idle and don't push a "continue"
184 // (it would queue behind them and could make Claude poll). Just wait for them.
185 const busy = busyAgents(await $.agent.list())
186 const waiting = busy.length > 0 && !(cur.deadline && now >= cur.deadline)
187 if (waiting) {
188 activity = 0
189 const d = await update($, drive, x => x && { ...x, lastActivity: `waiting on ${busy.length} agent${busy.length > 1 ? 's' : ''}: ${busy.slice(0, 2).join(', ')}` })
190 if (d) await persist($, d, now)
191 return r
192 }
193 const idle = activity === 0 ? cur.idle + 1 : 0
194 activity = 0
195 const limit =
196 cur.deadline && now >= cur.deadline ? 'deadline reached' :
197 cur.continues >= MAX_CONTINUES ? `${MAX_CONTINUES} auto-continues used` :
198 idle >= MAX_IDLE ? `${MAX_IDLE} turns in a row without progress` : undefined
199 if (limit) {
200 const d = await update($, drive, x => ended(x, 'limit', now, limit, { idle }))
201 if (d) await persist($, d, now)
202 $.ui.status(`■ self-drive stopped: ${limit} · /self-drive`)
203 void $.prompt.submit({ text: `Self-drive has stopped (${limit}). Stop working now. Write the final report for me: what was done, the decisions to review, and the needs-you list.`, asUser: true })
204 return r
205 }
206 const d = await update($, drive, x => x && { ...x, continues: x.continues + 1, idle })
207 if (d) await persist($, d, now)
208 void $.prompt.submit({ text: CONTINUE, asUser: true })
209 return r
210 })
211
212 // ---- the dashboard ----
213 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
214 const { Box, Text, Button, Markdown } = $.ui.resolve(e)
215 const d = await read($, drive)
216 if (!d) return <Text dimColor>No self-drive yet. Start one with /self-drive 2h <task></Text>
217 const now = Date.now()
218 const w = Math.max(20, e.props.bodyColumns - 2)
219 const running = d.status === 'driving'
220 const head = running ? { t: '▶ DRIVING', c: '#38bdf8' } : d.status === 'finished' ? { t: '✔ FINISHED', c: '#34d399' } : { t: '■ STOPPED', c: '#fbbf24' }
221 const ran = fmtDur((d.endedAt ?? now) - d.startedAt)
222 const budget = d.deadline ? ` of ${fmtDur(d.deadline - d.startedAt)}` : ''
223 const decisions = d.entries.filter(x => x.kind === 'decision')
224 const needs = d.entries.filter(x => x.kind === 'needs-you' || x.kind === 'held')
225 const fin = d.entries.filter(x => x.kind === 'finished').at(-1)
226 const notes = d.entries.filter(x => x.kind === 'note').slice(-3)
227
228 const row = (x: Entry, i: number, n?: number) => (
229 <Box key={`${x.kind}${i}`} flexDirection="column">
230 <Box flexDirection="row" columnGap={1}>
231 <Text color={KIND_STYLE[x.kind].color}>{n !== undefined ? `${n}.` : KIND_STYLE[x.kind].icon}</Text>
232 <Text bold wrap="wrap">{x.title}</Text>
233 </Box>
234 {x.why && <Text dimColor wrap="wrap"> why: {x.why}</Text>}
235 {x.alternatives && <Text dimColor wrap="wrap"> instead of: {x.alternatives}</Text>}
236 {x.kind !== 'decision' && x.detail && <Text dimColor wrap="wrap"> {x.detail.slice(0, w * 2)}</Text>}
237 </Box>
238 )
239
240 return (
241 <Box flexDirection="column" gap={1}>
242 <Box flexDirection="column">
243 <Box flexDirection="row" columnGap={2}>
244 <Text color={head.c} bold>{head.t}</Text>
245 <Text dimColor>⏱ {ran}{budget}</Text>
246 <Text dimColor>↻ {d.continues}</Text>
247 </Box>
248 <Text wrap="wrap">{d.task}</Text>
249 {d.endReason && !running && <Text dimColor>ended: {d.endReason}</Text>}
250 {running && d.lastActivity && <Text dimColor>now: {d.lastActivity.slice(0, w)}</Text>}
251 </Box>
252
253 {fin?.detail && (
254 <Box flexDirection="column">
255 <Text color="#34d399" bold>Summary</Text>
256 <Markdown text={fin.detail.slice(0, 3000)} />
257 </Box>
258 )}
259
260 <Box flexDirection="column">
261 <Text color="#38bdf8" bold>◆ Decisions made for you ({decisions.length})</Text>
262 {decisions.length === 0 ? <Text dimColor>none yet</Text> : decisions.map((x, i) => row(x, i, i + 1))}
263 </Box>
264
265 <Box flexDirection="column">
266 <Text color="#fbbf24" bold>✋ Needs you ({needs.length})</Text>
267 {needs.length === 0 ? <Text dimColor>nothing so far</Text> : needs.map((x, i) => row(x, i))}
268 </Box>
269
270 {notes.length > 0 && (
271 <Box flexDirection="column">
272 <Text dimColor bold>Milestones</Text>
273 {notes.map((x, i) => row(x, i))}
274 </Box>
275 )}
276
277 {running && (
278 <Button key="stop" label="Stop self-drive" onPress={async () => {
279 const t = Date.now()
280 const s = await update($, drive, x => ended(x, 'stopped', t, 'stopped by you'))
281 if (s) await persist($, s, t)
282 $.ui.status(undefined)
283 }} />
284 )}
285 </Box>
286 )
287 })
288}
289
290/** Saves the drive for resume and writes the readable report next to the other notes. */
291async function persist($: import('claude-code').EngineInterface, d: Drive, now: number): Promise<void> {
292 const sid = await $.session.id()
293 await $.store.set(`drive:${sid}`, d)
294 try {
295 const home = await $.env.get('HOME')
296 if (home) {
297 const day = new Date(d.startedAt).toISOString().slice(0, 10)
298 await $.fs.write(`${home}/.claude/self-drive/${day}-${sid.slice(0, 8)}.md`, report(d, now))
299 }
300 } catch { /* the report file is a convenience */ }
301}
302
303/** A tool already running cannot be cancelled here; no new work starts after the budget. */
304async function deadlineBlocks($: import('claude-code').EngineInterface, d: Drive | null): Promise<boolean> {
305 return d?.status === 'limit' || !!(d?.status === 'driving' && d.deadline !== undefined && await $.clock.now() >= d.deadline)
306}
307hooks/drive.ts 117 lines1import type { Drive, DriveStatus, Entry } from '../types'
2
3export const MAX_CONTINUES = 40
4export const MAX_IDLE = 3
5
6/** "/self-drive 3h task..." -> duration in ms (optional) and the task. */
7export function parseArgs(args: string): { ms?: number; task: string } {
8 const m = /^\s*(\d+(?:\.\d+)?)\s*(h|hr|hrs|hours?|m|min|mins|minutes?)\b[\s,;:.-]*/i.exec(args)
9 if (!m) return { task: args.trim() }
10 const n = Number(m[1])
11 const ms = /^h/i.test(m[2] ?? '') ? n * 3600000 : n * 60000
12 return { ms, task: args.slice(m[0].length).trim() }
13}
14
15export function brief(task: string, deadline: number | undefined, now: number): string {
16 const until = deadline ? `You have about ${fmtDur(deadline - now)} (until ${new Date(deadline).toTimeString().slice(0, 5)} local).` : 'Work until the goal is fully done.'
17 return `SELF-DRIVE MODE. I am stepping away and cannot answer anything until I return. ${until}
18
19Goal: ${task}
20
21How to work while I am away:
22- Never ask me anything and never wait for my acknowledgement. Make the best decision a careful senior engineer would, and keep going.
23- Every time you decide something you would normally have asked me about (a design choice, scope, a trade-off, an assumption, a default), record it with the self_drive_log tool, kind "decision": what you chose, the alternatives, and why. Be honest; these are what I will review first.
24- Anything that truly needs me (credentials, approvals, merges, production or shared-system actions, purchases, messages to people) — log it with kind "needs-you" and work around it; leave those for the end.
25- Risky, hard-to-undo or outward-facing actions are held by a guard while I am away. If one is refused, log it as needs-you and continue with everything else.
26- Plan the work, then execute it fully: implement, test, fix, verify. Prefer reversible steps. Commit locally in small steps where the repo allows it; do not push.
27- Use kind "note" sparingly for milestones.
28- When everything you can do is done, call self_drive_log with kind "finished" and a summary of what was done. Then write the final report for me: what was done, the decisions to review, and the needs-you list.`
29}
30
31export const CONTINUE = `Self-drive is still on and I am still away. Continue with the next step toward the goal. If everything you can do is done, call self_drive_log with kind "finished" and write the final report.`
32
33// ---- guard: actions held while away ----
34
35const RISKY: readonly [RegExp, string][] = [
36 [/\bgit\s+push\b[^\n]*(\s--force\b|\s-f\b|\s--force-with-lease\b)/, 'force push'],
37 [/\bgit\s+push\b[^\n]*\b(main|master)\b/, 'push to main/master'],
38 [/\bgit\s+push\b/, 'git push'],
39 [/\bgit\s+(branch\s+-D|push\s+\S+\s+--delete|push\s+\S+\s+:)/, 'delete a branch'],
40 [/\bgit\s+reset\s+--hard\b|\bgit\s+clean\s+-[a-z]*f/, 'discard work (reset --hard / clean -f)'],
41 [/\bkubectl\b[^\n]*\b(delete|apply|replace|patch|scale|rollout\s+restart|drain|cordon)\b/, 'change a cluster (kubectl)'],
42 [/\bhelm\s+(install|upgrade|uninstall|delete|rollback)\b/, 'change a cluster (helm)'],
43 [/\bterraform\s+(apply|destroy)\b|\bpulumi\s+(up|destroy)\b/, 'apply infrastructure'],
44 [/\bgh\s+(pr\s+(merge|close)|release\s+create|repo\s+delete)\b/, 'merge/close/release on GitHub'],
45 [/\b(npm|pnpm|yarn)\s+publish\b|\btwine\s+upload\b|\bdocker\s+push\b/, 'publish a package or image'],
46 [/\brm\s+-[a-z]*r[a-z]*f?\s+(\/(?!tmp\b)|~|\$HOME)/, 'recursive delete outside the project'],
47 [/\b(mgc|outlook)\b[^\n]*\b(send|reply|forward)\b|\bsendmail\b/, 'send email'],
48]
49
50/** The reason a Bash command is held, or undefined to let it run. */
51export function riskOf(command: string): string | undefined {
52 const token = String.raw`(?:"(?:\\.|[^"\\])*"|'[^']*'|[^\s;&|]+)`
53 const value = String.raw`(?:-C|-c|--git-dir|--work-tree|--namespace|--config-env)\s+${token}`
54 const attached = String.raw`(?:-[Cc][^\s;&|]+|--(?:git-dir|work-tree|namespace|config-env)=${token}|--(?:no-pager|paginate|bare|no-optional-locks|literal-pathspecs|glob-pathspecs|noglob-pathspecs|icase-pathspecs))`
55 const normalized = command.replace(new RegExp(String.raw`\bgit\s+(?:(?:${value}|${attached})\s+)+`, 'g'), 'git ')
56 for (const [re, why] of RISKY) if (re.test(normalized)) return why
57 return undefined
58}
59
60/** Tool names (MCP and skills) that reach people or shared systems. */
61export function riskyTool(tool: string): string | undefined {
62 if (/^mcp__.*(slack|teams|outlook|mail|send|post|jira.*(create|transition|comment|update)|confluence.*(create|update))/i.test(tool)) return 'post to a shared system or message people'
63 return undefined
64}
65
66// ---- report ----
67
68export function fmtDur(ms: number): string {
69 const m = Math.max(0, Math.round(ms / 60000))
70 return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, '0')}m`
71}
72
73const of = (d: Drive, k: Entry['kind']) => d.entries.filter(e => e.kind === k)
74
75export function report(d: Drive, now: number): string {
76 const end = d.endedAt ?? now
77 const lines = [
78 `# Self-drive report`,
79 ``,
80 `- Goal: ${d.task}`,
81 `- Status: ${d.status}${d.endReason ? ` (${d.endReason})` : ''}`,
82 `- Ran: ${fmtDur(end - d.startedAt)} · started ${new Date(d.startedAt).toLocaleString()}`,
83 `- Auto-continues: ${d.continues}`,
84 ]
85 const fin = of(d, 'finished').at(-1)
86 if (fin) lines.push('', '## Summary', '', fin.detail ?? fin.title)
87 const dec = of(d, 'decision')
88 lines.push('', `## Decisions made on your behalf (${dec.length})`, '')
89 dec.forEach((e, i) => {
90 lines.push(`${i + 1}. **${e.title}**`)
91 if (e.why) lines.push(` - Why: ${e.why}`)
92 if (e.alternatives) lines.push(` - Alternatives: ${e.alternatives}`)
93 if (e.detail) lines.push(` - ${e.detail}`)
94 })
95 const need = [...of(d, 'needs-you'), ...of(d, 'held')]
96 lines.push('', `## Needs you (${need.length})`, '')
97 need.forEach(e => lines.push(`- [ ] ${e.kind === 'held' ? '(held) ' : ''}${e.title}${e.detail ? ` — ${e.detail}` : ''}`))
98 const notes = of(d, 'note')
99 if (notes.length) { lines.push('', '## Milestones', ''); notes.forEach(e => lines.push(`- ${e.title}`)) }
100 return lines.join('\n') + '\n'
101}
102
103/** Ends a drive (typed, so callers can't widen the status). */
104export function ended(d: Drive | null, status: Exclude<DriveStatus, 'driving'>, at: number, reason: string, extra: Partial<Drive> = {}): Drive | null {
105 return d && { ...d, ...extra, status, endedAt: at, endReason: reason }
106}
107
108/** Appends one journal entry. */
109export function logged(d: Drive | null, e: Entry): Drive | null {
110 return d && { ...d, entries: [...d.entries, e] }
111}
112
113/** Agents still doing work: their completion will wake the main loop by itself. */
114export function busyAgents(list: readonly { status: string; description: string }[]): string[] {
115 return list.filter(a => a.status === 'pending' || a.status === 'running' || a.status === 'waiting').map(a => a.description)
116}
117types/index.d.ts 23 lines1export type EntryKind = 'decision' | 'needs-you' | 'held' | 'note' | 'finished'
2export type Entry = { at: number; kind: EntryKind; title: string; detail?: string; alternatives?: string; why?: string }
3export type DriveStatus = 'driving' | 'finished' | 'stopped' | 'limit'
4export type Drive = {
5 task: string
6 startedAt: number
7 deadline?: number
8 status: DriveStatus
9 endedAt?: number
10 endReason?: string
11 entries: Entry[]
12 continues: number
13 /** Consecutive auto-continued turns with no tool call and no log entry. */
14 idle: number
15 lastActivity?: string
16}
17
18declare module 'claude-code' {
19 interface PluginState {
20 'self-drive': { drive: Drive | null }
21 }
22}
23