Skip to content

Commit 8b4852a

Browse files
committed
fix(cli): harden self-improve status output
1 parent b790b93 commit 8b4852a

2 files changed

Lines changed: 80 additions & 8 deletions

File tree

packages/cli/src/ui/commands/selfImproveCommand.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,36 @@ describe('selfImproveCommand', () => {
137137
);
138138
expect((result as { content: string }).content).toContain('Cadence: 30m');
139139
});
140+
141+
it('does not print undefined for legacy primitive run refs', async () => {
142+
await selfImproveCommand.action?.(context, 'start --every 30m');
143+
const activeRaw = await fs.readFile(
144+
path.join(tempDir, '.qwen', 'self-improve', 'active.json'),
145+
'utf8',
146+
);
147+
const active = JSON.parse(activeRaw) as { activeLoopId: string };
148+
const statePath = path.join(
149+
tempDir,
150+
'.qwen',
151+
'self-improve',
152+
'loops',
153+
active.activeLoopId,
154+
'state.json',
155+
);
156+
const state = JSON.parse(await fs.readFile(statePath, 'utf8')) as Record<
157+
string,
158+
unknown
159+
>;
160+
state['status'] = 'completed_one_run';
161+
state['currentRun'] = 1;
162+
state['lastRun'] = '2026-05-15T02:02:00Z';
163+
await fs.writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`);
164+
165+
const result = await selfImproveCommand.action?.(context, 'status');
166+
const content = (result as { content: string }).content;
167+
expect(content).toContain('Status: completed_one_run');
168+
expect(content).toContain('Current run: 1');
169+
expect(content).toContain('Last run: 2026-05-15T02:02:00Z');
170+
expect(content).not.toContain('undefined');
171+
});
140172
});

packages/cli/src/ui/commands/selfImproveCommand.ts

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,38 @@ function describeSources(state: SelfImproveLoopState): string {
160160
return enabled.length === 0 ? 'none configured' : enabled.join(', ');
161161
}
162162

163+
function isRecord(value: unknown): value is Record<string, unknown> {
164+
return typeof value === 'object' && value !== null && !Array.isArray(value);
165+
}
166+
167+
function formatRunRef(value: unknown): string | null {
168+
if (value === undefined || value === null) return null;
169+
170+
if (isRecord(value)) {
171+
const runId = value['runId'];
172+
const status = value['status'];
173+
const runDoc = value['runDoc'];
174+
const parts: string[] = [];
175+
if (typeof runId === 'string' && runId.trim()) {
176+
parts.push(runId);
177+
}
178+
if (typeof status === 'string' && status.trim()) {
179+
parts.push(`(${status})`);
180+
}
181+
if (typeof runDoc === 'string' && runDoc.trim()) {
182+
parts.push(`- ${runDoc}`);
183+
}
184+
return parts.length > 0 ? parts.join(' ') : JSON.stringify(value);
185+
}
186+
187+
if (typeof value === 'string' && value.trim()) return value;
188+
if (typeof value === 'number' || typeof value === 'boolean') {
189+
return String(value);
190+
}
191+
192+
return null;
193+
}
194+
163195
function buildTickPrompt(state: SelfImproveLoopState): string {
164196
const loopDir = getSelfImproveLoopDir(state.repoRoot, state.loopId);
165197
return `You are running one tick of the built-in /self-improve loop.
@@ -190,6 +222,18 @@ Hard rules:
190222
9. Update ${path.join(loopDir, 'state.json')} as you progress, including currentRun, lastRun, stopRequested, and status.
191223
10. If stopRequested is true when you read the state, do not start a new run; mark the loop stopped if appropriate and stop.
192224
225+
State file schema rules:
226+
- status must be one of: "running", "stopping", "stopped", or "stale".
227+
- Keep status as "running" after a successful tick if the loop should continue.
228+
- currentRun and lastRun must be objects when present:
229+
{
230+
"runId": "001-short-slug",
231+
"status": "implementing | testing | success | failed | blocked | cancelled",
232+
"worktreePath": "/absolute/path/to/worktree",
233+
"runDoc": "/absolute/path/to/run.md"
234+
}
235+
- Do not write primitive values such as numbers or timestamps to currentRun or lastRun.
236+
193237
Task selection guidance:
194238
- If GitHub issues are enabled, use gh to inspect open issues and prefer clear, unclaimed, locally verifiable bugs or small enhancements.
195239
- If GitHub PRs are enabled, inspect relevant current-repo PRs for CI failures, review comments, and requested changes.
@@ -324,14 +368,10 @@ async function statusSelfImprove(config: Config): Promise<MessageActionReturn> {
324368
`Prompt: ${state.prompt || '(none)'}`,
325369
`Cron job: ${job ? job.id : 'none'}`,
326370
];
327-
if (state.currentRun) {
328-
lines.push(
329-
`Current run: ${state.currentRun.runId} (${state.currentRun.status})`,
330-
);
331-
}
332-
if (state.lastRun) {
333-
lines.push(`Last run: ${state.lastRun.runId} (${state.lastRun.status})`);
334-
}
371+
const currentRun = formatRunRef(state.currentRun);
372+
if (currentRun) lines.push(`Current run: ${currentRun}`);
373+
const lastRun = formatRunRef(state.lastRun);
374+
if (lastRun) lines.push(`Last run: ${lastRun}`);
335375
return message('info', lines.join('\n'));
336376
}
337377

0 commit comments

Comments
 (0)