@drewpayment/mink 0.1.0
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/README.md +347 -0
- package/package.json +32 -0
- package/src/cli.ts +176 -0
- package/src/commands/bug-search.ts +32 -0
- package/src/commands/config.ts +109 -0
- package/src/commands/cron.ts +295 -0
- package/src/commands/daemon.ts +46 -0
- package/src/commands/dashboard.ts +21 -0
- package/src/commands/designqc.ts +160 -0
- package/src/commands/detect-waste.ts +81 -0
- package/src/commands/framework-advisor.ts +52 -0
- package/src/commands/init.ts +159 -0
- package/src/commands/post-read.ts +123 -0
- package/src/commands/post-write.ts +157 -0
- package/src/commands/pre-read.ts +109 -0
- package/src/commands/pre-write.ts +136 -0
- package/src/commands/reflect.ts +39 -0
- package/src/commands/restore.ts +31 -0
- package/src/commands/scan.ts +101 -0
- package/src/commands/session-start.ts +21 -0
- package/src/commands/session-stop.ts +115 -0
- package/src/commands/status.ts +152 -0
- package/src/commands/update.ts +121 -0
- package/src/core/action-log.ts +341 -0
- package/src/core/backup.ts +122 -0
- package/src/core/bug-memory.ts +223 -0
- package/src/core/cron-parser.ts +94 -0
- package/src/core/daemon.ts +152 -0
- package/src/core/dashboard-api.ts +280 -0
- package/src/core/dashboard-server.ts +580 -0
- package/src/core/description.ts +232 -0
- package/src/core/design-eval/capture.ts +269 -0
- package/src/core/design-eval/route-detect.ts +165 -0
- package/src/core/design-eval/server-detect.ts +91 -0
- package/src/core/framework-advisor/catalog.ts +360 -0
- package/src/core/framework-advisor/decision-tree.ts +287 -0
- package/src/core/framework-advisor/generate.ts +132 -0
- package/src/core/framework-advisor/migration-prompts.ts +502 -0
- package/src/core/framework-advisor/validate.ts +137 -0
- package/src/core/fs-utils.ts +30 -0
- package/src/core/global-config.ts +74 -0
- package/src/core/index-store.ts +72 -0
- package/src/core/learning-memory.ts +120 -0
- package/src/core/paths.ts +86 -0
- package/src/core/pattern-engine.ts +108 -0
- package/src/core/project-id.ts +19 -0
- package/src/core/project-registry.ts +64 -0
- package/src/core/reflection.ts +256 -0
- package/src/core/scanner.ts +99 -0
- package/src/core/scheduler.ts +352 -0
- package/src/core/seed.ts +239 -0
- package/src/core/session.ts +128 -0
- package/src/core/stdin.ts +13 -0
- package/src/core/task-registry.ts +202 -0
- package/src/core/token-estimate.ts +36 -0
- package/src/core/token-ledger.ts +185 -0
- package/src/core/waste-detection.ts +214 -0
- package/src/core/write-exclusions.ts +24 -0
- package/src/types/action-log.ts +20 -0
- package/src/types/backup.ts +6 -0
- package/src/types/bug-memory.ts +24 -0
- package/src/types/config.ts +59 -0
- package/src/types/dashboard.ts +104 -0
- package/src/types/design-eval.ts +64 -0
- package/src/types/file-index.ts +38 -0
- package/src/types/framework-advisor.ts +97 -0
- package/src/types/hook-input.ts +27 -0
- package/src/types/learning-memory.ts +36 -0
- package/src/types/scheduler.ts +82 -0
- package/src/types/session.ts +50 -0
- package/src/types/token-ledger.ts +43 -0
- package/src/types/waste-detection.ts +21 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { CronSchedule } from "../types/scheduler";
|
|
2
|
+
|
|
3
|
+
// ── Field Parsing ───────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
function parseField(field: string, min: number, max: number): number[] {
|
|
6
|
+
const values = new Set<number>();
|
|
7
|
+
|
|
8
|
+
for (const part of field.split(",")) {
|
|
9
|
+
if (part === "*") {
|
|
10
|
+
for (let i = min; i <= max; i++) values.add(i);
|
|
11
|
+
} else if (part.includes("/")) {
|
|
12
|
+
const [range, stepStr] = part.split("/");
|
|
13
|
+
const step = parseInt(stepStr, 10);
|
|
14
|
+
if (isNaN(step) || step <= 0) {
|
|
15
|
+
throw new Error(`Invalid step value: ${part}`);
|
|
16
|
+
}
|
|
17
|
+
const start = range === "*" ? min : parseInt(range, 10);
|
|
18
|
+
if (isNaN(start) || start < min || start > max) {
|
|
19
|
+
throw new Error(`Invalid range start: ${part}`);
|
|
20
|
+
}
|
|
21
|
+
for (let i = start; i <= max; i += step) values.add(i);
|
|
22
|
+
} else if (part.includes("-")) {
|
|
23
|
+
const [lo, hi] = part.split("-").map(Number);
|
|
24
|
+
if (isNaN(lo) || isNaN(hi) || lo < min || hi > max || lo > hi) {
|
|
25
|
+
throw new Error(`Invalid range: ${part}`);
|
|
26
|
+
}
|
|
27
|
+
for (let i = lo; i <= hi; i++) values.add(i);
|
|
28
|
+
} else {
|
|
29
|
+
const n = parseInt(part, 10);
|
|
30
|
+
if (isNaN(n) || n < min || n > max) {
|
|
31
|
+
throw new Error(`Invalid value: ${part} (must be ${min}-${max})`);
|
|
32
|
+
}
|
|
33
|
+
values.add(n);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return [...values].sort((a, b) => a - b);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export function parseCronExpression(expr: string): CronSchedule {
|
|
43
|
+
const fields = expr.trim().split(/\s+/);
|
|
44
|
+
if (fields.length !== 5) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Invalid cron expression: expected 5 fields, got ${fields.length}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
minute: parseField(fields[0], 0, 59),
|
|
52
|
+
hour: parseField(fields[1], 0, 23),
|
|
53
|
+
dayOfMonth: parseField(fields[2], 1, 31),
|
|
54
|
+
month: parseField(fields[3], 1, 12),
|
|
55
|
+
dayOfWeek: parseField(fields[4], 0, 6),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function matches(schedule: CronSchedule, date: Date): boolean {
|
|
60
|
+
return (
|
|
61
|
+
schedule.minute.includes(date.getUTCMinutes()) &&
|
|
62
|
+
schedule.hour.includes(date.getUTCHours()) &&
|
|
63
|
+
schedule.dayOfMonth.includes(date.getUTCDate()) &&
|
|
64
|
+
schedule.month.includes(date.getUTCMonth() + 1) &&
|
|
65
|
+
schedule.dayOfWeek.includes(date.getUTCDay())
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function nextRunAfter(schedule: CronSchedule, after: Date): Date {
|
|
70
|
+
// Start from the next whole minute
|
|
71
|
+
const candidate = new Date(after.getTime());
|
|
72
|
+
candidate.setUTCSeconds(0, 0);
|
|
73
|
+
candidate.setUTCMinutes(candidate.getUTCMinutes() + 1);
|
|
74
|
+
|
|
75
|
+
const limit = after.getTime() + 366 * 24 * 60 * 60 * 1000;
|
|
76
|
+
|
|
77
|
+
while (candidate.getTime() <= limit) {
|
|
78
|
+
if (matches(schedule, candidate)) {
|
|
79
|
+
return candidate;
|
|
80
|
+
}
|
|
81
|
+
candidate.setUTCMinutes(candidate.getUTCMinutes() + 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new Error("No matching time found within 366 days");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isInCurrentPeriod(
|
|
88
|
+
schedule: CronSchedule,
|
|
89
|
+
lastRun: Date,
|
|
90
|
+
now: Date
|
|
91
|
+
): boolean {
|
|
92
|
+
const next = nextRunAfter(schedule, lastRun);
|
|
93
|
+
return now.getTime() < next.getTime();
|
|
94
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync, openSync } from "fs";
|
|
2
|
+
import { mkdirSync } from "fs";
|
|
3
|
+
import { dirname, resolve } from "path";
|
|
4
|
+
import { schedulerPidPath, schedulerLogPath } from "./paths";
|
|
5
|
+
import type { PidFileData } from "../types/scheduler";
|
|
6
|
+
|
|
7
|
+
// ── PID File Operations ─────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export function readPidFile(): PidFileData | null {
|
|
10
|
+
try {
|
|
11
|
+
const raw = readFileSync(schedulerPidPath(), "utf-8");
|
|
12
|
+
const data = JSON.parse(raw);
|
|
13
|
+
if (
|
|
14
|
+
data &&
|
|
15
|
+
typeof data.pid === "number" &&
|
|
16
|
+
typeof data.startedAt === "string" &&
|
|
17
|
+
typeof data.projectCwd === "string"
|
|
18
|
+
) {
|
|
19
|
+
return data as PidFileData;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function writePidFile(data: PidFileData): void {
|
|
28
|
+
const pidPath = schedulerPidPath();
|
|
29
|
+
mkdirSync(dirname(pidPath), { recursive: true });
|
|
30
|
+
writeFileSync(pidPath, JSON.stringify(data, null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function removePidFile(): void {
|
|
34
|
+
try {
|
|
35
|
+
unlinkSync(schedulerPidPath());
|
|
36
|
+
} catch {
|
|
37
|
+
// Already removed
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isProcessAlive(pid: number): boolean {
|
|
42
|
+
try {
|
|
43
|
+
process.kill(pid, 0);
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Daemon Lifecycle ────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export function startDaemon(cwd: string): void {
|
|
53
|
+
const existing = readPidFile();
|
|
54
|
+
if (existing && isProcessAlive(existing.pid)) {
|
|
55
|
+
console.log(
|
|
56
|
+
`[mink] scheduler is already running (PID: ${existing.pid})`
|
|
57
|
+
);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Clean up stale PID file
|
|
62
|
+
if (existing) {
|
|
63
|
+
removePidFile();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Resolve the CLI entry point
|
|
67
|
+
const cliPath = resolve(import.meta.dir, "../cli.ts");
|
|
68
|
+
|
|
69
|
+
// Ensure log directory exists
|
|
70
|
+
const logPath = schedulerLogPath();
|
|
71
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
72
|
+
const logFd = openSync(logPath, "a");
|
|
73
|
+
|
|
74
|
+
const proc = Bun.spawn(["bun", "run", cliPath, "cron", "__daemon"], {
|
|
75
|
+
cwd,
|
|
76
|
+
stdout: logFd,
|
|
77
|
+
stderr: logFd,
|
|
78
|
+
stdin: "ignore",
|
|
79
|
+
env: process.env,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Unref so parent can exit
|
|
83
|
+
proc.unref();
|
|
84
|
+
|
|
85
|
+
writePidFile({
|
|
86
|
+
pid: proc.pid,
|
|
87
|
+
startedAt: new Date().toISOString(),
|
|
88
|
+
projectCwd: cwd,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log(`[mink] scheduler started (PID: ${proc.pid})`);
|
|
92
|
+
console.log(`[mink] log: ${logPath}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function stopDaemon(): Promise<void> {
|
|
96
|
+
const pidData = readPidFile();
|
|
97
|
+
if (!pidData) {
|
|
98
|
+
console.log("[mink] scheduler is not running (no PID file)");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!isProcessAlive(pidData.pid)) {
|
|
103
|
+
console.log("[mink] scheduler is not running (stale PID file)");
|
|
104
|
+
removePidFile();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Send SIGTERM
|
|
109
|
+
process.kill(pidData.pid, "SIGTERM");
|
|
110
|
+
console.log(`[mink] sent SIGTERM to PID ${pidData.pid}`);
|
|
111
|
+
|
|
112
|
+
// Poll for up to 5 seconds
|
|
113
|
+
for (let i = 0; i < 50; i++) {
|
|
114
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
115
|
+
if (!isProcessAlive(pidData.pid)) {
|
|
116
|
+
removePidFile();
|
|
117
|
+
console.log("[mink] scheduler stopped");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Force kill
|
|
123
|
+
try {
|
|
124
|
+
process.kill(pidData.pid, "SIGKILL");
|
|
125
|
+
} catch {
|
|
126
|
+
// Process may have just exited
|
|
127
|
+
}
|
|
128
|
+
removePidFile();
|
|
129
|
+
console.log("[mink] scheduler force-stopped (SIGKILL)");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getDaemonStatus(cwd: string): {
|
|
133
|
+
running: boolean;
|
|
134
|
+
pid?: number;
|
|
135
|
+
startedAt?: string;
|
|
136
|
+
projectCwd?: string;
|
|
137
|
+
} {
|
|
138
|
+
const pidData = readPidFile();
|
|
139
|
+
if (!pidData) {
|
|
140
|
+
return { running: false };
|
|
141
|
+
}
|
|
142
|
+
if (!isProcessAlive(pidData.pid)) {
|
|
143
|
+
removePidFile();
|
|
144
|
+
return { running: false };
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
running: true,
|
|
148
|
+
pid: pidData.pid,
|
|
149
|
+
startedAt: pidData.startedAt,
|
|
150
|
+
projectCwd: pidData.projectCwd,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import {
|
|
3
|
+
projectDir,
|
|
4
|
+
fileIndexPath,
|
|
5
|
+
tokenLedgerPath,
|
|
6
|
+
bugMemoryPath,
|
|
7
|
+
actionLogPath,
|
|
8
|
+
learningMemoryPath,
|
|
9
|
+
projectMetaPath,
|
|
10
|
+
sessionPath,
|
|
11
|
+
configPath,
|
|
12
|
+
schedulerManifestPath,
|
|
13
|
+
designReportPath,
|
|
14
|
+
} from "./paths";
|
|
15
|
+
import { safeReadJson } from "./fs-utils";
|
|
16
|
+
import { isFileIndex } from "./index-store";
|
|
17
|
+
import { loadLedger } from "./token-ledger";
|
|
18
|
+
import { parseLearningMemory } from "./learning-memory";
|
|
19
|
+
import { loadBugMemory } from "./bug-memory";
|
|
20
|
+
import { safeReadLog, parseLogSessions } from "./action-log";
|
|
21
|
+
import { getDaemonStatus } from "./daemon";
|
|
22
|
+
import { loadManifest, removeFromDeadLetter, saveManifest } from "./scheduler";
|
|
23
|
+
import { getBuiltInTasks, executeTask } from "./task-registry";
|
|
24
|
+
import type {
|
|
25
|
+
OverviewPayload,
|
|
26
|
+
TokenLedgerPayload,
|
|
27
|
+
FileIndexPayload,
|
|
28
|
+
SchedulerPayload,
|
|
29
|
+
BugLogPayload,
|
|
30
|
+
ActionLogPayload,
|
|
31
|
+
ActionResult,
|
|
32
|
+
FileStatus,
|
|
33
|
+
DesignPayload,
|
|
34
|
+
} from "../types/dashboard";
|
|
35
|
+
import { isDesignEvalReport } from "../types/design-eval";
|
|
36
|
+
import type { DesignEvalReport } from "../types/design-eval";
|
|
37
|
+
import type { FileIndex, FileIndexEntry } from "../types/file-index";
|
|
38
|
+
import type { LearningMemory } from "../types/learning-memory";
|
|
39
|
+
|
|
40
|
+
// ── File Status Checks ─────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function checkJsonFile(
|
|
43
|
+
name: string,
|
|
44
|
+
filePath: string,
|
|
45
|
+
validator?: (v: unknown) => boolean
|
|
46
|
+
): FileStatus {
|
|
47
|
+
if (!existsSync(filePath)) return { name, status: "missing" };
|
|
48
|
+
const data = safeReadJson(filePath);
|
|
49
|
+
if (data === null) return { name, status: "corrupt" };
|
|
50
|
+
if (validator && !validator(data)) return { name, status: "corrupt" };
|
|
51
|
+
return { name, status: "ok" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function checkTextFile(name: string, filePath: string): FileStatus {
|
|
55
|
+
if (!existsSync(filePath)) return { name, status: "missing" };
|
|
56
|
+
try {
|
|
57
|
+
readFileSync(filePath, "utf-8");
|
|
58
|
+
return { name, status: "ok" };
|
|
59
|
+
} catch {
|
|
60
|
+
return { name, status: "corrupt" };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Panel Loaders ──────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export function loadOverview(cwd: string): OverviewPayload {
|
|
67
|
+
// Project metadata
|
|
68
|
+
let project: OverviewPayload["project"] = null;
|
|
69
|
+
const meta = safeReadJson(projectMetaPath(cwd)) as {
|
|
70
|
+
name?: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
} | null;
|
|
73
|
+
if (meta && typeof meta === "object") {
|
|
74
|
+
project = {
|
|
75
|
+
name: meta.name ?? "Unknown",
|
|
76
|
+
description: meta.description ?? "",
|
|
77
|
+
cwd,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Daemon status
|
|
82
|
+
const daemonStatus = getDaemonStatus(cwd);
|
|
83
|
+
const daemon: OverviewPayload["daemon"] = {
|
|
84
|
+
running: daemonStatus.running,
|
|
85
|
+
pid: daemonStatus.pid,
|
|
86
|
+
startedAt: daemonStatus.startedAt,
|
|
87
|
+
uptimeMs: daemonStatus.startedAt
|
|
88
|
+
? Date.now() - new Date(daemonStatus.startedAt).getTime()
|
|
89
|
+
: undefined,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Token ledger summary
|
|
93
|
+
const ledger = loadLedger(tokenLedgerPath(cwd));
|
|
94
|
+
const summary = {
|
|
95
|
+
totalSessions: ledger.lifetime.totalSessions,
|
|
96
|
+
totalTokens: ledger.lifetime.totalTokens,
|
|
97
|
+
totalReads: ledger.lifetime.totalReads,
|
|
98
|
+
totalWrites: ledger.lifetime.totalWrites,
|
|
99
|
+
estimatedSavings: ledger.lifetime.totalEstimatedSavings,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// State file health
|
|
103
|
+
const stateFiles: FileStatus[] = [
|
|
104
|
+
checkJsonFile("session.json", sessionPath(cwd)),
|
|
105
|
+
checkJsonFile("file-index.json", fileIndexPath(cwd), isFileIndex),
|
|
106
|
+
checkJsonFile("config.json", configPath(cwd)),
|
|
107
|
+
checkTextFile("learning-memory.md", learningMemoryPath(cwd)),
|
|
108
|
+
checkJsonFile("token-ledger.json", tokenLedgerPath(cwd)),
|
|
109
|
+
checkJsonFile("bug-memory.json", bugMemoryPath(cwd)),
|
|
110
|
+
checkTextFile("action-log.md", actionLogPath(cwd)),
|
|
111
|
+
checkJsonFile("scheduler-manifest.json", schedulerManifestPath(cwd)),
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
return { project, daemon, summary, stateFiles };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function loadTokenLedgerPanel(cwd: string): TokenLedgerPayload {
|
|
118
|
+
const ledger = loadLedger(tokenLedgerPath(cwd));
|
|
119
|
+
return {
|
|
120
|
+
lifetime: ledger.lifetime,
|
|
121
|
+
sessions: ledger.sessions,
|
|
122
|
+
wasteFlags: ledger.wasteFlags ?? [],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function loadFileIndexPanel(cwd: string): FileIndexPayload {
|
|
127
|
+
const raw = safeReadJson(fileIndexPath(cwd));
|
|
128
|
+
if (raw && isFileIndex(raw)) {
|
|
129
|
+
const index = raw as FileIndex;
|
|
130
|
+
const entries: FileIndexEntry[] = Object.values(index.entries);
|
|
131
|
+
return { header: index.header, entries };
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
header: {
|
|
135
|
+
lastScanTimestamp: "",
|
|
136
|
+
totalFiles: 0,
|
|
137
|
+
lifetimeHits: 0,
|
|
138
|
+
lifetimeMisses: 0,
|
|
139
|
+
},
|
|
140
|
+
entries: [],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function loadSchedulerPanel(cwd: string): SchedulerPayload {
|
|
145
|
+
const manifest = loadManifest(cwd);
|
|
146
|
+
const definitions = getBuiltInTasks();
|
|
147
|
+
|
|
148
|
+
const tasks = definitions.map((def) => {
|
|
149
|
+
const state = manifest?.tasks.find((t) => t.taskId === def.id) ?? null;
|
|
150
|
+
return { definition: def, state };
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
tasks,
|
|
155
|
+
deadLetterQueue: manifest?.deadLetterQueue ?? [],
|
|
156
|
+
lastHeartbeat: manifest?.lastHeartbeat ?? null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function loadLearningMemoryPanel(cwd: string): LearningMemory {
|
|
161
|
+
const memPath = learningMemoryPath(cwd);
|
|
162
|
+
if (!existsSync(memPath)) {
|
|
163
|
+
return {
|
|
164
|
+
projectName: "unknown",
|
|
165
|
+
sections: {
|
|
166
|
+
"User Preferences": [],
|
|
167
|
+
"Key Learnings": [],
|
|
168
|
+
"Do-Not-Repeat": [],
|
|
169
|
+
"Decision Log": [],
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const content = readFileSync(memPath, "utf-8");
|
|
175
|
+
return parseLearningMemory(content);
|
|
176
|
+
} catch {
|
|
177
|
+
return {
|
|
178
|
+
projectName: "unknown",
|
|
179
|
+
sections: {
|
|
180
|
+
"User Preferences": [],
|
|
181
|
+
"Key Learnings": [],
|
|
182
|
+
"Do-Not-Repeat": [],
|
|
183
|
+
"Decision Log": [],
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function loadActionLogPanel(cwd: string): ActionLogPayload {
|
|
190
|
+
const content = safeReadLog(actionLogPath(cwd));
|
|
191
|
+
const sessions = parseLogSessions(content);
|
|
192
|
+
return { sessions };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function loadBugLogPanel(cwd: string): BugLogPayload {
|
|
196
|
+
const memory = loadBugMemory(bugMemoryPath(cwd));
|
|
197
|
+
return { entries: memory.entries, nextId: memory.nextId };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function loadDesignPanel(cwd: string): DesignPayload {
|
|
201
|
+
const raw = safeReadJson(designReportPath(cwd));
|
|
202
|
+
if (!raw || !isDesignEvalReport(raw)) {
|
|
203
|
+
return { images: [] };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const report = raw as DesignEvalReport;
|
|
207
|
+
return {
|
|
208
|
+
images: report.captures.map((c) => ({
|
|
209
|
+
url: `/api/design-images/${c.fileName}`,
|
|
210
|
+
route: c.route,
|
|
211
|
+
viewport: c.viewport,
|
|
212
|
+
section: c.section,
|
|
213
|
+
timestamp: c.timestamp,
|
|
214
|
+
})),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Action Triggers ────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
export async function triggerTask(
|
|
221
|
+
cwd: string,
|
|
222
|
+
taskId: string
|
|
223
|
+
): Promise<ActionResult> {
|
|
224
|
+
try {
|
|
225
|
+
await executeTask(taskId, cwd);
|
|
226
|
+
return { success: true };
|
|
227
|
+
} catch (err) {
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
error: err instanceof Error ? err.message : String(err),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function triggerDeadLetterRetry(
|
|
236
|
+
cwd: string,
|
|
237
|
+
taskId: string
|
|
238
|
+
): Promise<ActionResult> {
|
|
239
|
+
try {
|
|
240
|
+
const manifest = loadManifest(cwd);
|
|
241
|
+
if (!manifest) {
|
|
242
|
+
return { success: false, error: "No scheduler manifest found" };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const entry = removeFromDeadLetter(manifest, taskId);
|
|
246
|
+
if (!entry) {
|
|
247
|
+
return { success: false, error: `Task ${taskId} not in dead letter queue` };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Reset the task record
|
|
251
|
+
const record = manifest.tasks.find((t) => t.taskId === taskId);
|
|
252
|
+
if (record) {
|
|
253
|
+
record.status = "idle";
|
|
254
|
+
record.consecutiveFailures = 0;
|
|
255
|
+
record.currentAttempt = 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
saveManifest(cwd, manifest);
|
|
259
|
+
await executeTask(taskId, cwd);
|
|
260
|
+
return { success: true };
|
|
261
|
+
} catch (err) {
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
error: err instanceof Error ? err.message : String(err),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function triggerRescan(cwd: string): Promise<ActionResult> {
|
|
270
|
+
try {
|
|
271
|
+
const { scan } = await import("../commands/scan");
|
|
272
|
+
scan(cwd, { check: false });
|
|
273
|
+
return { success: true };
|
|
274
|
+
} catch (err) {
|
|
275
|
+
return {
|
|
276
|
+
success: false,
|
|
277
|
+
error: err instanceof Error ? err.message : String(err),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|