Skip to content

Commit 8647fea

Browse files
author
StackMemory Bot (CLI)
committed
feat(conductor): write status file + dashboard widget
- Conductor writes .stackmemory/conductor-status.json on each poll cycle - Dashboard reads status file and renders conductor panel with running agents, completion stats, and stale detection - Add tsconfig.check.json for src-only type checking - Status file is cleared on conductor stop
1 parent 223855d commit 8647fea

File tree

3 files changed

+156
-2
lines changed

3 files changed

+156
-2
lines changed

src/cli/commands/dashboard.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { SessionManager } from '../../core/session/session-manager.js';
99
import { FrameManager } from '../../core/context/index.js';
1010
import Database from 'better-sqlite3';
1111
import { join } from 'path';
12-
import { existsSync } from 'fs';
12+
import { existsSync, readFileSync } from 'fs';
1313
import { getModelTokenLimit } from '../../core/models/model-router.js';
1414

1515
/** Frame statistics row */
@@ -216,6 +216,82 @@ export const dashboardCommand = {
216216
console.log(`${usageBar} ${contextUsage}%`);
217217
console.log();
218218

219+
// Conductor Status
220+
const conductorStatusPath = join(
221+
projectRoot,
222+
'.stackmemory',
223+
'conductor-status.json'
224+
);
225+
if (existsSync(conductorStatusPath)) {
226+
try {
227+
const raw = readFileSync(conductorStatusPath, 'utf-8');
228+
const conductorStatus = JSON.parse(raw) as {
229+
startedAt: number;
230+
updatedAt: number;
231+
running: Array<{
232+
identifier: string;
233+
title: string;
234+
status: string;
235+
runtime: number;
236+
}>;
237+
queued: number;
238+
completed: number;
239+
failed: number;
240+
maxConcurrent: number;
241+
stopping: boolean;
242+
};
243+
244+
const stale = Date.now() - conductorStatus.updatedAt > 120000;
245+
const header = stale
246+
? chalk.gray.bold('⚙ Conductor (stale)')
247+
: chalk.yellow.bold('⚙ Conductor');
248+
249+
console.log(header);
250+
251+
if (conductorStatus.running.length > 0) {
252+
const conductorTable = new Table({
253+
head: [
254+
chalk.white('Issue'),
255+
chalk.white('Status'),
256+
chalk.white('Title'),
257+
chalk.white('Runtime'),
258+
],
259+
style: { head: [], border: [] },
260+
colWidths: [12, 14, 36, 10],
261+
});
262+
263+
conductorStatus.running.forEach((r) => {
264+
const mins = Math.round(r.runtime / 60000);
265+
const statusColor =
266+
r.status === 'running'
267+
? chalk.green
268+
: r.status === 'completed'
269+
? chalk.cyan
270+
: chalk.red;
271+
conductorTable.push([
272+
r.identifier,
273+
statusColor(r.status),
274+
r.title.slice(0, 34),
275+
`${mins}m`,
276+
]);
277+
});
278+
279+
console.log(conductorTable.toString());
280+
} else {
281+
console.log(chalk.gray(' No agents running'));
282+
}
283+
284+
console.log(
285+
chalk.gray(
286+
` Completed: ${conductorStatus.completed} Failed: ${conductorStatus.failed} Max: ${conductorStatus.maxConcurrent}`
287+
)
288+
);
289+
console.log();
290+
} catch {
291+
// Ignore corrupt status file
292+
}
293+
}
294+
219295
db.close();
220296

221297
// Footer

src/cli/commands/orchestrator.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*/
1010

1111
import { spawn, execSync, type ChildProcess } from 'child_process';
12-
import { existsSync, mkdirSync, rmSync } from 'fs';
12+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
1313
import { join, dirname } from 'path';
1414
import { tmpdir } from 'os';
1515
import { fileURLToPath } from 'url';
@@ -222,6 +222,8 @@ export class Conductor {
222222
`Orchestrator started — polling every ${this.config.pollIntervalMs / 1000}s, max ${this.config.maxConcurrent} concurrent`
223223
);
224224

225+
this.writeStatusFile();
226+
225227
// Register signal handlers
226228
const shutdown = () => this.stop();
227229
process.on('SIGINT', shutdown);
@@ -283,6 +285,7 @@ export class Conductor {
283285

284286
this.running.clear();
285287
this.claimed.clear();
288+
this.clearStatusFile();
286289

287290
console.log(
288291
`Orchestrator stopped. Completed: ${this.completeCount}, Failed: ${this.failCount}`
@@ -310,6 +313,61 @@ export class Conductor {
310313
};
311314
}
312315

316+
// ── Status File ──
317+
318+
/**
319+
* Write current conductor state to .stackmemory/conductor-status.json
320+
* for consumption by `stackmemory dashboard` and other tools.
321+
*/
322+
private writeStatusFile(): void {
323+
const statusDir = join(this.config.repoRoot, '.stackmemory');
324+
if (!existsSync(statusDir)) return;
325+
326+
const status = {
327+
pid: process.pid,
328+
startedAt: this.startedAt,
329+
updatedAt: Date.now(),
330+
running: Array.from(this.running.values()).map((r) => ({
331+
identifier: r.issue.identifier,
332+
title: r.issue.title,
333+
status: r.status,
334+
attempt: r.attempt,
335+
startedAt: r.startedAt,
336+
runtime: Date.now() - r.startedAt,
337+
})),
338+
queued: Array.from(this.claimed).filter(
339+
(id) => !this.running.has(id) && !this.completed.has(id)
340+
).length,
341+
completed: this.completeCount,
342+
failed: this.failCount,
343+
totalAttempts: this.totalAttempts,
344+
maxConcurrent: this.config.maxConcurrent,
345+
stopping: this.stopping,
346+
};
347+
348+
try {
349+
writeFileSync(
350+
join(statusDir, 'conductor-status.json'),
351+
JSON.stringify(status, null, 2)
352+
);
353+
} catch {
354+
// Non-fatal — status file is best-effort
355+
}
356+
}
357+
358+
private clearStatusFile(): void {
359+
const statusPath = join(
360+
this.config.repoRoot,
361+
'.stackmemory',
362+
'conductor-status.json'
363+
);
364+
try {
365+
if (existsSync(statusPath)) rmSync(statusPath);
366+
} catch {
367+
// Non-fatal
368+
}
369+
}
370+
313371
// ── Polling ──
314372

315373
private async schedulePoll(): Promise<void> {
@@ -325,6 +383,7 @@ export class Conductor {
325383
} catch (err) {
326384
logger.error('Poll cycle failed', { error: (err as Error).message });
327385
}
386+
this.writeStatusFile();
328387
}
329388
}
330389

@@ -525,6 +584,7 @@ export class Conductor {
525584
console.log(`[${issue.identifier}] Failed: ${(err as Error).message}`);
526585
} finally {
527586
this.running.delete(issueId);
587+
this.writeStatusFile();
528588
// Keep claimed so we don't re-dispatch within this session
529589
}
530590
}

tsconfig.check.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"noEmit": true,
5+
"declaration": false,
6+
"declarationMap": false,
7+
"sourceMap": false,
8+
"incremental": true,
9+
"tsBuildInfoFile": ".tsbuildinfo"
10+
},
11+
"include": ["src/**/*"],
12+
"exclude": [
13+
"node_modules",
14+
"dist",
15+
"**/*.test.ts",
16+
"**/__tests__/**"
17+
]
18+
}

0 commit comments

Comments
 (0)