Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 25 additions & 18 deletions src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { fileURLToPath } from "url";
import { run, runUserMessage, bootstrap, ensureProjectClaudeMd, loadHeartbeatPromptTemplate } from "../runner";
import { writeState, type StateData } from "../statusline";
import { cronMatches, nextCronMatch } from "../cron";
import { clearJobSchedule, loadJobs } from "../jobs";
import { clearJobSchedule, loadJobs, snapshotJobFrontmatter } from "../jobs";
import { writePidFile, cleanupPidFile, checkExistingDaemon } from "../pid";
import { initConfig, loadSettings, reloadSettings, resolvePrompt, type HeartbeatConfig, type Settings } from "../config";
import { getDayAndMinuteAtOffset } from "../timezone";
Expand Down Expand Up @@ -693,23 +693,30 @@ export async function start(args: string[] = []) {
const now = new Date();
for (const job of currentJobs) {
if (cronMatches(job.schedule, now, currentSettings.timezoneOffsetMinutes)) {
resolvePrompt(job.prompt)
.then((prompt) => run(job.name, prompt))
.then((r) => {
if (job.notify === false) return;
if (job.notify === "error" && r.exitCode === 0) return;
forwardToTelegram(job.name, r);
forwardToDiscord(job.name, r);
})
.finally(async () => {
if (job.recurring) return;
try {
await clearJobSchedule(job.name);
console.log(`[${ts()}] Cleared schedule for one-time job: ${job.name}`);
} catch (err) {
console.error(`[${ts()}] Failed to clear schedule for ${job.name}:`, err);
}
});
snapshotJobFrontmatter(job.name)
.then((restoreFrontmatter) =>
resolvePrompt(job.prompt)
.then((prompt) => run(job.name, prompt))
.then(async (r) => {
const restored = await restoreFrontmatter();
if (restored) {
console.log(`[${ts()}] Restored frontmatter for job: ${job.name}`);
}
if (job.notify === false) return;
if (job.notify === "error" && r.exitCode === 0) return;
forwardToTelegram(job.name, r);
forwardToDiscord(job.name, r);
})
.finally(async () => {
if (job.recurring) return;
try {
await clearJobSchedule(job.name);
console.log(`[${ts()}] Cleared schedule for one-time job: ${job.name}`);
} catch (err) {
console.error(`[${ts()}] Failed to clear schedule for ${job.name}:`, err);
}
})
);
}
}
updateState();
Expand Down
53 changes: 53 additions & 0 deletions src/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,59 @@ export async function loadJobs(): Promise<Job[]> {
return jobs;
}

/**
* Snapshot a job file's frontmatter before execution.
* Returns a restore function that re-applies the original frontmatter
* if Claude overwrote or stripped it during the run.
*/
export async function snapshotJobFrontmatter(
jobName: string
): Promise<() => Promise<boolean>> {
const path = join(JOBS_DIR, `${jobName}.md`);
let originalContent: string;
try {
originalContent = await Bun.file(path).text();
} catch {
return async () => false;
}

const originalMatch = originalContent.match(
/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/
);
if (!originalMatch) return async () => false;

const originalFrontmatter = originalMatch[1];

return async () => {
let currentContent: string;
try {
currentContent = await Bun.file(path).text();
} catch {
return false;
}

const currentMatch = currentContent.match(
/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/
);

// File completely mangled — restore entire original
if (!currentMatch) {
await Bun.write(path, originalContent);
return true;
}

// Frontmatter was modified — restore it, keep current body
if (currentMatch[1].trim() !== originalFrontmatter.trim()) {
const restoredBody = currentMatch[2].trim();
const restored = `---\n${originalFrontmatter}\n---\n${restoredBody}\n`;
await Bun.write(path, restored);
return true;
}

return false;
};
}

export async function clearJobSchedule(jobName: string): Promise<void> {
const path = join(JOBS_DIR, `${jobName}.md`);
const content = await Bun.file(path).text();
Expand Down