@damian87/omp 0.5.0 → 0.7.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/.github/skills/schedule/SKILL.md +71 -0
- package/.github/skills/team/SKILL.md +82 -57
- package/.github/skills/team/scripts/team-launch.sh +140 -42
- package/README.md +21 -2
- package/catalog/skills-general.json +2 -2
- package/dist/src/cli.d.ts +1 -7
- package/dist/src/cli.js +128 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/registry.d.ts +3 -0
- package/dist/src/commands/registry.js +11 -0
- package/dist/src/commands/registry.js.map +1 -0
- package/dist/src/commands/suggest.d.ts +19 -0
- package/dist/src/commands/suggest.js +158 -0
- package/dist/src/commands/suggest.js.map +1 -0
- package/dist/src/commands/types.d.ts +16 -0
- package/dist/src/commands/types.js +2 -0
- package/dist/src/commands/types.js.map +1 -0
- package/dist/src/instructions-memory.js +8 -15
- package/dist/src/instructions-memory.js.map +1 -1
- package/dist/src/schedule/commands.d.ts +34 -0
- package/dist/src/schedule/commands.js +130 -0
- package/dist/src/schedule/commands.js.map +1 -0
- package/dist/src/schedule/installer.d.ts +20 -0
- package/dist/src/schedule/installer.js +76 -0
- package/dist/src/schedule/installer.js.map +1 -0
- package/dist/src/schedule/installers/crontab.d.ts +17 -0
- package/dist/src/schedule/installers/crontab.js +112 -0
- package/dist/src/schedule/installers/crontab.js.map +1 -0
- package/dist/src/schedule/installers/launchd.d.ts +22 -0
- package/dist/src/schedule/installers/launchd.js +125 -0
- package/dist/src/schedule/installers/launchd.js.map +1 -0
- package/dist/src/schedule/installers/systemd.d.ts +15 -0
- package/dist/src/schedule/installers/systemd.js +136 -0
- package/dist/src/schedule/installers/systemd.js.map +1 -0
- package/dist/src/schedule/job-store.d.ts +21 -0
- package/dist/src/schedule/job-store.js +102 -0
- package/dist/src/schedule/job-store.js.map +1 -0
- package/dist/src/schedule/lock.d.ts +9 -0
- package/dist/src/schedule/lock.js +83 -0
- package/dist/src/schedule/lock.js.map +1 -0
- package/dist/src/schedule/paths.d.ts +15 -0
- package/dist/src/schedule/paths.js +36 -0
- package/dist/src/schedule/paths.js.map +1 -0
- package/dist/src/schedule/runner.d.ts +8 -0
- package/dist/src/schedule/runner.js +151 -0
- package/dist/src/schedule/runner.js.map +1 -0
- package/dist/src/schedule/types.d.ts +60 -0
- package/dist/src/schedule/types.js +5 -0
- package/dist/src/schedule/types.js.map +1 -0
- package/dist/src/team/index.d.ts +1 -0
- package/dist/src/team/index.js +1 -0
- package/dist/src/team/index.js.map +1 -1
- package/dist/src/team/pane-monitor.d.ts +39 -0
- package/dist/src/team/pane-monitor.js +128 -0
- package/dist/src/team/pane-monitor.js.map +1 -0
- package/dist/src/team/runtime.js +12 -1
- package/dist/src/team/runtime.js.map +1 -1
- package/dist/src/team/tmux.d.ts +12 -0
- package/dist/src/team/tmux.js +38 -0
- package/dist/src/team/tmux.js.map +1 -1
- package/docs/research/2026-06-01-schedule-cron-feature.md +346 -0
- package/package.json +2 -1
- package/scripts/lib/schedule-results.mjs +88 -0
- package/scripts/session-start.mjs +8 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ScheduleJob } from "../types.js";
|
|
2
|
+
export declare function unitBaseName(id: string): string;
|
|
3
|
+
export declare function servicePath(id: string): string;
|
|
4
|
+
export declare function timerPath(id: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Translate a 5-field cron to a systemd OnCalendar expression. Covers the
|
|
7
|
+
* common patterns (every-N-min, every-N-hours, daily/weekly at H:M including
|
|
8
|
+
* day-of-week); approximates the rest with a best-effort date-time mapping.
|
|
9
|
+
*/
|
|
10
|
+
export declare function cronToSystemdCalendar(cron: string): string;
|
|
11
|
+
export declare function generateService(job: ScheduleJob, stateRoot: string): string;
|
|
12
|
+
export declare function generateTimer(job: ScheduleJob): string;
|
|
13
|
+
export declare function installSystemd(job: ScheduleJob, stateRoot: string): void;
|
|
14
|
+
export declare function uninstallSystemd(id: string): void;
|
|
15
|
+
export declare function statusSystemd(id: string): boolean;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
export function unitBaseName(id) {
|
|
6
|
+
return `omp-schedule-${id}`;
|
|
7
|
+
}
|
|
8
|
+
function userUnitDir() {
|
|
9
|
+
return join(homedir(), ".config", "systemd", "user");
|
|
10
|
+
}
|
|
11
|
+
export function servicePath(id) {
|
|
12
|
+
return join(userUnitDir(), `${unitBaseName(id)}.service`);
|
|
13
|
+
}
|
|
14
|
+
export function timerPath(id) {
|
|
15
|
+
return join(userUnitDir(), `${unitBaseName(id)}.timer`);
|
|
16
|
+
}
|
|
17
|
+
const STEP_EVERY = /^\*\/(\d+)$/;
|
|
18
|
+
const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
19
|
+
function dayName(n) {
|
|
20
|
+
const v = Number(n);
|
|
21
|
+
if (!Number.isInteger(v))
|
|
22
|
+
return undefined;
|
|
23
|
+
return DAY_NAMES[v % 7]; // cron allows 0 and 7 for Sunday
|
|
24
|
+
}
|
|
25
|
+
/** Translate a cron day-of-week field to a systemd day prefix (empty when wild/unparseable). */
|
|
26
|
+
function dowToSystemd(dow) {
|
|
27
|
+
if (dow === "*")
|
|
28
|
+
return "";
|
|
29
|
+
if (/^\d+$/.test(dow))
|
|
30
|
+
return dayName(dow) ?? "";
|
|
31
|
+
const range = /^(\d+)-(\d+)$/.exec(dow);
|
|
32
|
+
if (range) {
|
|
33
|
+
const a = dayName(range[1]);
|
|
34
|
+
const b = dayName(range[2]);
|
|
35
|
+
return a && b ? `${a}..${b}` : "";
|
|
36
|
+
}
|
|
37
|
+
if (dow.includes(",")) {
|
|
38
|
+
const names = dow.split(",").map((d) => dayName(d.trim()));
|
|
39
|
+
if (names.every(Boolean))
|
|
40
|
+
return names.join(",");
|
|
41
|
+
}
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Translate a 5-field cron to a systemd OnCalendar expression. Covers the
|
|
46
|
+
* common patterns (every-N-min, every-N-hours, daily/weekly at H:M including
|
|
47
|
+
* day-of-week); approximates the rest with a best-effort date-time mapping.
|
|
48
|
+
*/
|
|
49
|
+
export function cronToSystemdCalendar(cron) {
|
|
50
|
+
const parts = cron.trim().split(/\s+/);
|
|
51
|
+
if (parts.length !== 5)
|
|
52
|
+
return cron;
|
|
53
|
+
const [min, hour, dom, mon, dow] = parts;
|
|
54
|
+
const minStep = STEP_EVERY.exec(min);
|
|
55
|
+
if (minStep && hour === "*" && dom === "*" && mon === "*" && dow === "*") {
|
|
56
|
+
return `*:0/${minStep[1]}`; // every N minutes
|
|
57
|
+
}
|
|
58
|
+
const hourStep = STEP_EVERY.exec(hour);
|
|
59
|
+
if (min === "0" && hourStep && dom === "*" && mon === "*" && dow === "*") {
|
|
60
|
+
return `0/${hourStep[1]}:00`; // every N hours
|
|
61
|
+
}
|
|
62
|
+
const pad = (s) => (/^\d+$/.test(s) ? s.padStart(2, "0") : "*");
|
|
63
|
+
const time = `${pad(hour)}:${pad(min)}:00`;
|
|
64
|
+
const dayPrefix = dowToSystemd(dow);
|
|
65
|
+
const prefix = dayPrefix ? `${dayPrefix} ` : "";
|
|
66
|
+
if (dom === "*" && mon === "*") {
|
|
67
|
+
return `${prefix}*-*-* ${time}`;
|
|
68
|
+
}
|
|
69
|
+
// best effort with date components
|
|
70
|
+
return `${prefix}*-${pad(mon)}-${pad(dom)} ${time}`;
|
|
71
|
+
}
|
|
72
|
+
/** Double-quote a value for a systemd unit (handles spaces; escapes backslash and quote). */
|
|
73
|
+
function sdq(s) {
|
|
74
|
+
return `"${s.replace(/(["\\])/g, "\\$1")}"`;
|
|
75
|
+
}
|
|
76
|
+
export function generateService(job, stateRoot) {
|
|
77
|
+
// Run from stateRoot with --root so state resolves independent of agent cwd.
|
|
78
|
+
return `[Unit]
|
|
79
|
+
Description=omp scheduled job ${job.id}
|
|
80
|
+
|
|
81
|
+
[Service]
|
|
82
|
+
Type=oneshot
|
|
83
|
+
ExecStart=${sdq(job.ompBinPath)} schedule run --id ${job.id} --root ${sdq(stateRoot)}
|
|
84
|
+
WorkingDirectory=${sdq(stateRoot)}
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
export function generateTimer(job) {
|
|
88
|
+
return `[Unit]
|
|
89
|
+
Description=Timer for omp scheduled job ${job.id}
|
|
90
|
+
|
|
91
|
+
[Timer]
|
|
92
|
+
OnCalendar=${cronToSystemdCalendar(job.cron)}
|
|
93
|
+
Persistent=true
|
|
94
|
+
|
|
95
|
+
[Install]
|
|
96
|
+
WantedBy=timers.target
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
export function installSystemd(job, stateRoot) {
|
|
100
|
+
mkdirSync(userUnitDir(), { recursive: true });
|
|
101
|
+
writeFileSync(servicePath(job.id), generateService(job, stateRoot), "utf8");
|
|
102
|
+
writeFileSync(timerPath(job.id), generateTimer(job), "utf8");
|
|
103
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
104
|
+
execFileSync("systemctl", ["--user", "enable", "--now", `${unitBaseName(job.id)}.timer`], { stdio: "ignore" });
|
|
105
|
+
}
|
|
106
|
+
export function uninstallSystemd(id) {
|
|
107
|
+
try {
|
|
108
|
+
execFileSync("systemctl", ["--user", "disable", "--now", `${unitBaseName(id)}.timer`], { stdio: "ignore" });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// already disabled
|
|
112
|
+
}
|
|
113
|
+
for (const p of [servicePath(id), timerPath(id)]) {
|
|
114
|
+
if (existsSync(p))
|
|
115
|
+
unlinkSync(p);
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// best effort
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function statusSystemd(id) {
|
|
125
|
+
try {
|
|
126
|
+
const out = execFileSync("systemctl", ["--user", "is-active", `${unitBaseName(id)}.timer`], {
|
|
127
|
+
encoding: "utf8",
|
|
128
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
129
|
+
});
|
|
130
|
+
return out.trim() === "active";
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return existsSync(timerPath(id));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=systemd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemd.js","sourceRoot":"","sources":["../../../../src/schedule/installers/systemd.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ScheduleJob, ScheduleRunResult } from "./types.js";
|
|
2
|
+
/** Atomic write (tmp + rename), mirroring task-store.writeTask. */
|
|
3
|
+
export declare function writeJob(jobPath: string, job: ScheduleJob): void;
|
|
4
|
+
export declare function readJob(jobPath: string): ScheduleJob | undefined;
|
|
5
|
+
export declare function listJobs(jobsDir: string): ScheduleJob[];
|
|
6
|
+
export declare function deleteJob(jobPath: string): void;
|
|
7
|
+
/** Append a result line. Append-only — never read-modify-write (race-free under concurrent runs). */
|
|
8
|
+
export declare function appendRunResult(resultsPath: string, result: ScheduleRunResult): void;
|
|
9
|
+
/** Atomic write of the byte-offset cursor (tmp + rename). */
|
|
10
|
+
export declare function advanceCursor(cursorPath: string, bytes: number): void;
|
|
11
|
+
export interface ResultsScan {
|
|
12
|
+
results: ScheduleRunResult[];
|
|
13
|
+
newCursor: number;
|
|
14
|
+
cursor: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Read result lines from the stored byte offset to EOF, parsing only complete
|
|
18
|
+
* lines. Mirrors outbox.scanFromCursor. `maxBytes` bounds the read so a hook
|
|
19
|
+
* with a tight time budget never scans an unbounded file.
|
|
20
|
+
*/
|
|
21
|
+
export declare function readResultsFrom(resultsPath: string, cursorPath: string, maxBytes?: number): ResultsScan;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
/** Atomic write (tmp + rename), mirroring task-store.writeTask. */
|
|
4
|
+
export function writeJob(jobPath, job) {
|
|
5
|
+
mkdirSync(dirname(jobPath), { recursive: true });
|
|
6
|
+
const tmp = `${jobPath}.tmp.${process.pid}.${Date.now()}`;
|
|
7
|
+
writeFileSync(tmp, JSON.stringify(job, null, 2), "utf8");
|
|
8
|
+
renameSync(tmp, jobPath);
|
|
9
|
+
}
|
|
10
|
+
export function readJob(jobPath) {
|
|
11
|
+
if (!existsSync(jobPath))
|
|
12
|
+
return undefined;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(jobPath, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function listJobs(jobsDir) {
|
|
21
|
+
if (!existsSync(jobsDir))
|
|
22
|
+
return [];
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const entry of readdirSync(jobsDir, { withFileTypes: true })) {
|
|
25
|
+
if (!entry.isFile() || !entry.name.endsWith(".json"))
|
|
26
|
+
continue;
|
|
27
|
+
const job = readJob(join(jobsDir, entry.name));
|
|
28
|
+
if (job)
|
|
29
|
+
out.push(job);
|
|
30
|
+
}
|
|
31
|
+
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
32
|
+
}
|
|
33
|
+
export function deleteJob(jobPath) {
|
|
34
|
+
if (existsSync(jobPath))
|
|
35
|
+
unlinkSync(jobPath);
|
|
36
|
+
}
|
|
37
|
+
/** Append a result line. Append-only — never read-modify-write (race-free under concurrent runs). */
|
|
38
|
+
export function appendRunResult(resultsPath, result) {
|
|
39
|
+
mkdirSync(dirname(resultsPath), { recursive: true });
|
|
40
|
+
appendFileSync(resultsPath, `${JSON.stringify(result)}\n`, "utf8");
|
|
41
|
+
}
|
|
42
|
+
function readCursorBytes(cursorPath) {
|
|
43
|
+
if (!existsSync(cursorPath))
|
|
44
|
+
return 0;
|
|
45
|
+
try {
|
|
46
|
+
const data = JSON.parse(readFileSync(cursorPath, "utf8"));
|
|
47
|
+
return Number(data.bytesRead) || 0;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Atomic write of the byte-offset cursor (tmp + rename). */
|
|
54
|
+
export function advanceCursor(cursorPath, bytes) {
|
|
55
|
+
mkdirSync(dirname(cursorPath), { recursive: true });
|
|
56
|
+
const tmp = `${cursorPath}.tmp.${process.pid}.${Date.now()}`;
|
|
57
|
+
writeFileSync(tmp, JSON.stringify({ bytesRead: bytes }), "utf8");
|
|
58
|
+
renameSync(tmp, cursorPath);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read result lines from the stored byte offset to EOF, parsing only complete
|
|
62
|
+
* lines. Mirrors outbox.scanFromCursor. `maxBytes` bounds the read so a hook
|
|
63
|
+
* with a tight time budget never scans an unbounded file.
|
|
64
|
+
*/
|
|
65
|
+
export function readResultsFrom(resultsPath, cursorPath, maxBytes = 16_384) {
|
|
66
|
+
let cursor = readCursorBytes(cursorPath);
|
|
67
|
+
if (!existsSync(resultsPath))
|
|
68
|
+
return { results: [], newCursor: cursor, cursor };
|
|
69
|
+
const stats = statSync(resultsPath);
|
|
70
|
+
if (cursor > stats.size)
|
|
71
|
+
cursor = 0; // file truncated/rotated → re-read from start
|
|
72
|
+
if (cursor >= stats.size)
|
|
73
|
+
return { results: [], newCursor: cursor, cursor };
|
|
74
|
+
const remaining = Math.min(stats.size - cursor, maxBytes);
|
|
75
|
+
const fd = openSync(resultsPath, "r");
|
|
76
|
+
const buf = Buffer.alloc(remaining);
|
|
77
|
+
try {
|
|
78
|
+
readSync(fd, buf, 0, remaining, cursor);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
closeSync(fd);
|
|
82
|
+
}
|
|
83
|
+
const text = buf.toString("utf8");
|
|
84
|
+
const lastNewline = text.lastIndexOf("\n");
|
|
85
|
+
if (lastNewline === -1)
|
|
86
|
+
return { results: [], newCursor: cursor, cursor };
|
|
87
|
+
const consumed = text.slice(0, lastNewline + 1);
|
|
88
|
+
const newCursor = cursor + Buffer.byteLength(consumed, "utf8");
|
|
89
|
+
const results = [];
|
|
90
|
+
for (const line of consumed.split("\n")) {
|
|
91
|
+
if (!line.trim())
|
|
92
|
+
continue;
|
|
93
|
+
try {
|
|
94
|
+
results.push(JSON.parse(line));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// ignore unparseable line; cursor still advances past it
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { results, newCursor, cursor };
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=job-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-store.js","sourceRoot":"","sources":["../../../src/schedule/job-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface LockHandle {
|
|
2
|
+
acquired: boolean;
|
|
3
|
+
release: () => void;
|
|
4
|
+
}
|
|
5
|
+
/** Acquire an exclusive lock via `openSync(..., 'wx')` (mirrors task-store). */
|
|
6
|
+
export declare function acquireLock(lockPath: string): LockHandle;
|
|
7
|
+
/** A lock is stale if its owner PID is dead or it is older than 2× maxAgeMs. */
|
|
8
|
+
export declare function isLockStale(lockPath: string, maxAgeMs: number): boolean;
|
|
9
|
+
export declare function forceReleaseStaleLock(lockPath: string, maxAgeMs: number): void;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
function readLock(lockPath) {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(readFileSync(lockPath, "utf8"));
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Acquire an exclusive lock via `openSync(..., 'wx')` (mirrors task-store). */
|
|
13
|
+
export function acquireLock(lockPath) {
|
|
14
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
15
|
+
let fd;
|
|
16
|
+
try {
|
|
17
|
+
fd = openSync(lockPath, "wx");
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return { acquired: false, release: () => { } };
|
|
21
|
+
}
|
|
22
|
+
const token = randomUUID();
|
|
23
|
+
try {
|
|
24
|
+
writeFileSync(lockPath, JSON.stringify({ pid: process.pid, acquiredAt: new Date().toISOString(), token }));
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
closeSync(fd);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
acquired: true,
|
|
31
|
+
// Only delete the lock if it is still OURS — a stale-steal may have replaced
|
|
32
|
+
// it with a newer holder's lock, which we must not unlink.
|
|
33
|
+
release: () => {
|
|
34
|
+
if (readLock(lockPath)?.token === token) {
|
|
35
|
+
try {
|
|
36
|
+
unlinkSync(lockPath);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// ignore: already gone
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function pidAlive(pid) {
|
|
46
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
47
|
+
return false;
|
|
48
|
+
try {
|
|
49
|
+
process.kill(pid, 0);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
// ESRCH => no such process (dead). EPERM => exists but not ours (alive).
|
|
54
|
+
return err.code === "EPERM";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** A lock is stale if its owner PID is dead or it is older than 2× maxAgeMs. */
|
|
58
|
+
export function isLockStale(lockPath, maxAgeMs) {
|
|
59
|
+
if (!existsSync(lockPath))
|
|
60
|
+
return false;
|
|
61
|
+
let data;
|
|
62
|
+
try {
|
|
63
|
+
data = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return true; // unparseable lock is stale
|
|
67
|
+
}
|
|
68
|
+
if (!pidAlive(data.pid))
|
|
69
|
+
return true;
|
|
70
|
+
const age = Date.now() - Date.parse(data.acquiredAt);
|
|
71
|
+
return Number.isFinite(age) && age > 2 * maxAgeMs;
|
|
72
|
+
}
|
|
73
|
+
export function forceReleaseStaleLock(lockPath, maxAgeMs) {
|
|
74
|
+
if (isLockStale(lockPath, maxAgeMs)) {
|
|
75
|
+
try {
|
|
76
|
+
unlinkSync(lockPath);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../../src/schedule/lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC9G,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAapC,SAAS,QAAQ,CAAC,QAAgB;IAChC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAa,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,IAAI,EAAU,CAAC;IACf,IAAI,CAAC;QACH,EAAE,GAAG,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC;IAChD,CAAC;IACD,MAAM,KAAK,GAAG,UAAU,EAAE,CAAC;IAC3B,IAAI,CAAC;QACH,aAAa,CACX,QAAQ,EACR,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,KAAK,EAAqB,CAAC,CACrG,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,SAAS,CAAC,EAAE,CAAC,CAAC;IAChB,CAAC;IACD,OAAO;QACL,QAAQ,EAAE,IAAI;QACd,6EAA6E;QAC7E,2DAA2D;QAC3D,OAAO,EAAE,GAAG,EAAE;YACZ,IAAI,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,KAAK,KAAK,EAAE,CAAC;gBACxC,IAAI,CAAC;oBACH,UAAU,CAAC,QAAQ,CAAC,CAAC;gBACvB,CAAC;gBAAC,MAAM,CAAC;oBACP,uBAAuB;gBACzB,CAAC;YACH,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,GAAW;IAC3B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACrD,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,yEAAyE;QACzE,OAAQ,GAA6B,CAAC,IAAI,KAAK,OAAO,CAAC;IACzD,CAAC;AACH,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,WAAW,CAAC,QAAgB,EAAE,QAAgB;IAC5D,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IACxC,IAAI,IAAc,CAAC;IACnB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAa,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,CAAC,4BAA4B;IAC3C,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACrD,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,GAAG,QAAQ,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,QAAgB,EAAE,QAAgB;IACtE,IAAI,WAAW,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC;QACpC,IAAI,CAAC;YACH,UAAU,CAAC,QAAQ,CAAC,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface SchedulePaths {
|
|
2
|
+
cwd: string;
|
|
3
|
+
scheduleRoot: string;
|
|
4
|
+
jobsDir: string;
|
|
5
|
+
logsDir: string;
|
|
6
|
+
resultsDir: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveSchedulePaths(cwd: string): SchedulePaths;
|
|
9
|
+
export declare function jobFilePath(jobsDir: string, id: string): string;
|
|
10
|
+
export declare function jobLockPath(jobsDir: string, id: string): string;
|
|
11
|
+
export declare function runLogDir(logsDir: string, id: string): string;
|
|
12
|
+
export declare function resultsFilePath(resultsDir: string, id: string): string;
|
|
13
|
+
/** Byte-offset cursor for "seen" results (mirrors team outbox `.offset`). */
|
|
14
|
+
export declare function resultsCursorPath(resultsDir: string, id: string): string;
|
|
15
|
+
export declare function ensureScheduleDirs(paths: SchedulePaths): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
export function resolveSchedulePaths(cwd) {
|
|
4
|
+
const root = resolve(cwd);
|
|
5
|
+
const scheduleRoot = join(root, ".omp", "state", "schedule");
|
|
6
|
+
return {
|
|
7
|
+
cwd: root,
|
|
8
|
+
scheduleRoot,
|
|
9
|
+
jobsDir: join(scheduleRoot, "jobs"),
|
|
10
|
+
logsDir: join(scheduleRoot, "logs"),
|
|
11
|
+
resultsDir: join(scheduleRoot, "results"),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function jobFilePath(jobsDir, id) {
|
|
15
|
+
return join(jobsDir, `${id}.json`);
|
|
16
|
+
}
|
|
17
|
+
export function jobLockPath(jobsDir, id) {
|
|
18
|
+
return join(jobsDir, `${id}.lock`);
|
|
19
|
+
}
|
|
20
|
+
export function runLogDir(logsDir, id) {
|
|
21
|
+
return join(logsDir, id);
|
|
22
|
+
}
|
|
23
|
+
export function resultsFilePath(resultsDir, id) {
|
|
24
|
+
return join(resultsDir, `${id}.jsonl`);
|
|
25
|
+
}
|
|
26
|
+
/** Byte-offset cursor for "seen" results (mirrors team outbox `.offset`). */
|
|
27
|
+
export function resultsCursorPath(resultsDir, id) {
|
|
28
|
+
return join(resultsDir, `${id}.offset`);
|
|
29
|
+
}
|
|
30
|
+
export function ensureScheduleDirs(paths) {
|
|
31
|
+
for (const dir of [paths.scheduleRoot, paths.jobsDir, paths.logsDir, paths.resultsDir]) {
|
|
32
|
+
if (!existsSync(dir))
|
|
33
|
+
mkdirSync(dir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../src/schedule/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAU1C,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAC1B,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;IAC7D,OAAO;QACL,GAAG,EAAE,IAAI;QACT,YAAY;QACZ,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC;QACnC,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC;QACnC,UAAU,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC;KAC1C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,EAAU;IACrD,OAAO,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,EAAU;IACrD,OAAO,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,OAAe,EAAE,EAAU;IACnD,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,UAAkB,EAAE,EAAU;IAC5D,OAAO,IAAI,CAAC,UAAU,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;AACzC,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,iBAAiB,CAAC,UAAkB,EAAE,EAAU;IAC9D,OAAO,IAAI,CAAC,UAAU,EAAE,GAAG,EAAE,SAAS,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAoB;IACrD,KAAK,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;QACvF,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type SchedulePaths } from "./paths.js";
|
|
2
|
+
import { type ScheduleJob, type ScheduleRunResult } from "./types.js";
|
|
3
|
+
export interface RunOptions {
|
|
4
|
+
/** Called when a job has expired (TTL passed or maxRuns reached) so the caller can uninstall the OS entry. */
|
|
5
|
+
onExpire?: (job: ScheduleJob) => void;
|
|
6
|
+
}
|
|
7
|
+
/** Execute one scheduled run (expiry check, overlap lock, timeout, log capture, result append). */
|
|
8
|
+
export declare function runScheduledJob(job: ScheduleJob, paths: SchedulePaths, opts?: RunOptions): Promise<ScheduleRunResult>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { resolveCopilotBin } from "../copilot/launch.js";
|
|
5
|
+
import { appendRunResult, readJob, writeJob } from "./job-store.js";
|
|
6
|
+
import { acquireLock, forceReleaseStaleLock, isLockStale } from "./lock.js";
|
|
7
|
+
import { jobFilePath, jobLockPath, resultsFilePath, runLogDir } from "./paths.js";
|
|
8
|
+
import { DEFAULT_TIMEOUT_MS } from "./types.js";
|
|
9
|
+
function isExpired(job) {
|
|
10
|
+
if (job.maxRuns !== undefined && job.runCount >= job.maxRuns)
|
|
11
|
+
return true;
|
|
12
|
+
if (job.expiresAt && Date.now() > Date.parse(job.expiresAt))
|
|
13
|
+
return true;
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
function timestampSlug() {
|
|
17
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
18
|
+
}
|
|
19
|
+
/** Keep only the newest `keep` per-run logs in a job's log dir (timestamp slugs sort chronologically). */
|
|
20
|
+
function rotateLogs(logDir, keep = 50) {
|
|
21
|
+
if (!existsSync(logDir))
|
|
22
|
+
return;
|
|
23
|
+
const files = readdirSync(logDir)
|
|
24
|
+
.filter((f) => f.endsWith(".log"))
|
|
25
|
+
.sort();
|
|
26
|
+
for (const f of files.slice(0, Math.max(0, files.length - keep))) {
|
|
27
|
+
try {
|
|
28
|
+
unlinkSync(join(logDir, f));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// best effort
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Execute one scheduled run (expiry check, overlap lock, timeout, log capture, result append). */
|
|
36
|
+
export async function runScheduledJob(job, paths, opts = {}) {
|
|
37
|
+
const timeoutMs = job.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
38
|
+
const jobPath = jobFilePath(paths.jobsDir, job.id);
|
|
39
|
+
const resultsPath = resultsFilePath(paths.resultsDir, job.id);
|
|
40
|
+
const persist = (status, result, incrementRun) => {
|
|
41
|
+
appendRunResult(resultsPath, result);
|
|
42
|
+
const current = readJob(jobPath) ?? job;
|
|
43
|
+
writeJob(jobPath, {
|
|
44
|
+
...current,
|
|
45
|
+
runCount: incrementRun ? current.runCount + 1 : current.runCount,
|
|
46
|
+
lastRunAt: result.ts,
|
|
47
|
+
lastStatus: status,
|
|
48
|
+
lastSummary: result.summary,
|
|
49
|
+
lastLogPath: result.logPath,
|
|
50
|
+
active: status === "expired" ? false : current.active,
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
// 1. Expiry / max-runs check BEFORE any spawn.
|
|
54
|
+
if (isExpired(job)) {
|
|
55
|
+
const result = {
|
|
56
|
+
ts: new Date().toISOString(),
|
|
57
|
+
exitCode: 0,
|
|
58
|
+
status: "expired",
|
|
59
|
+
summary: "job expired (TTL or max-runs reached); deactivated",
|
|
60
|
+
logPath: "",
|
|
61
|
+
durationMs: 0,
|
|
62
|
+
};
|
|
63
|
+
persist("expired", result, false);
|
|
64
|
+
opts.onExpire?.(job);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
// 2. Overlap lock.
|
|
68
|
+
const lockPath = jobLockPath(paths.jobsDir, job.id);
|
|
69
|
+
forceReleaseStaleLock(lockPath, timeoutMs);
|
|
70
|
+
let lock = acquireLock(lockPath);
|
|
71
|
+
if (!lock.acquired) {
|
|
72
|
+
if (isLockStale(lockPath, timeoutMs)) {
|
|
73
|
+
forceReleaseStaleLock(lockPath, timeoutMs);
|
|
74
|
+
lock = acquireLock(lockPath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!lock.acquired) {
|
|
78
|
+
const result = {
|
|
79
|
+
ts: new Date().toISOString(),
|
|
80
|
+
exitCode: -1,
|
|
81
|
+
status: "locked",
|
|
82
|
+
summary: "previous run still in progress; skipped",
|
|
83
|
+
logPath: "",
|
|
84
|
+
durationMs: 0,
|
|
85
|
+
};
|
|
86
|
+
appendRunResult(resultsPath, result);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
// 3. Spawn the agent and capture output.
|
|
90
|
+
const startedAt = Date.now();
|
|
91
|
+
const logDir = runLogDir(paths.logsDir, job.id);
|
|
92
|
+
mkdirSync(logDir, { recursive: true });
|
|
93
|
+
const logPath = join(logDir, `${timestampSlug()}.log`);
|
|
94
|
+
try {
|
|
95
|
+
const bin = resolveCopilotBin(job.bin);
|
|
96
|
+
const args = [];
|
|
97
|
+
if (job.model)
|
|
98
|
+
args.push("--model", job.model);
|
|
99
|
+
args.push("-p", job.prompt);
|
|
100
|
+
if (job.allowAllTools)
|
|
101
|
+
args.push("--allow-all-tools");
|
|
102
|
+
const result = await new Promise((resolveFn) => {
|
|
103
|
+
const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"], cwd: job.cwd });
|
|
104
|
+
let stdout = "";
|
|
105
|
+
let stderr = "";
|
|
106
|
+
let timedOut = false;
|
|
107
|
+
let settled = false;
|
|
108
|
+
let killTimer;
|
|
109
|
+
const timer = setTimeout(() => {
|
|
110
|
+
timedOut = true;
|
|
111
|
+
child.kill("SIGTERM");
|
|
112
|
+
// Escalate to SIGKILL if the child ignores SIGTERM, so a run can't hang forever.
|
|
113
|
+
killTimer = setTimeout(() => child.kill("SIGKILL"), 5000);
|
|
114
|
+
}, timeoutMs);
|
|
115
|
+
child.stdout?.on("data", (d) => {
|
|
116
|
+
stdout += d.toString();
|
|
117
|
+
});
|
|
118
|
+
child.stderr?.on("data", (d) => {
|
|
119
|
+
stderr += d.toString();
|
|
120
|
+
});
|
|
121
|
+
const finish = (exitCode) => {
|
|
122
|
+
if (settled)
|
|
123
|
+
return;
|
|
124
|
+
settled = true;
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
if (killTimer)
|
|
127
|
+
clearTimeout(killTimer);
|
|
128
|
+
writeFileSync(logPath, `$ ${bin} ${args.join(" ")}\n\n[stdout]\n${stdout}\n[stderr]\n${stderr}\n`, "utf8");
|
|
129
|
+
rotateLogs(logDir);
|
|
130
|
+
const status = timedOut ? "timeout" : exitCode === 0 ? "ok" : "error";
|
|
131
|
+
const summarySource = (stdout.trim() || stderr.trim()).replace(/\s+/g, " ");
|
|
132
|
+
resolveFn({
|
|
133
|
+
ts: new Date().toISOString(),
|
|
134
|
+
exitCode,
|
|
135
|
+
status,
|
|
136
|
+
summary: summarySource.slice(0, 200) || `exit ${exitCode}`,
|
|
137
|
+
logPath,
|
|
138
|
+
durationMs: Date.now() - startedAt,
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
child.on("error", () => finish(127));
|
|
142
|
+
child.on("close", (code) => finish(typeof code === "number" ? code : timedOut ? 124 : 1));
|
|
143
|
+
});
|
|
144
|
+
persist(result.status, result, true);
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
lock.release();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../src/schedule/runner.ts"],"names":[],"mappings":"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"}
|