@minionry/minion 0.7.36 → 0.7.39
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/bin/commands/minions.js +19 -0
- package/bin/minion.js +33 -4
- package/dist/server/cli/headless/claude-invoker-process.js +23 -7
- package/dist/server/cli/headless/claude-invoker-stream.js +8 -3
- package/dist/server/cli/headless/haiku-assessments.js +2 -1
- package/dist/server/cli/headless/mcp-config.js +2 -1
- package/dist/server/engines/claude/claude-command.js +1 -0
- package/dist/server/engines/factory.js +2 -0
- package/dist/server/engines/hermes/HermesEngine.js +112 -219
- package/dist/server/engines/hermes/hermes-gateway-registry.js +467 -0
- package/dist/server/index.js +10 -4
- package/dist/server/mcp/bouncer-integration.js +3 -2
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +35 -8
- package/dist/server/mcp/classifier/shadow-eval-scheduler.js +42 -0
- package/dist/server/mcp/classifier/telemetry.js +0 -0
- package/dist/server/mcp/permission-channels.js +176 -0
- package/dist/server/mcp/security-analysis.js +9 -1
- package/dist/server/mcp/security-audit.js +22 -2
- package/dist/server/mcp/security-patterns.js +73 -1
- package/dist/server/mcp/server.js +80 -50
- package/dist/server/routes/internal.js +6 -1
- package/dist/server/routes/notifications.js +6 -18
- package/dist/server/server-setup.js +24 -48
- package/dist/server/services/analytics.js +4 -2
- package/dist/server/services/browser/host-mock.js +6 -1
- package/dist/server/services/browser/host.js +27 -3
- package/dist/server/services/browser/reasoning-agent.js +3 -1
- package/dist/server/services/chain/chain-engine.js +494 -0
- package/dist/server/services/chain/chain-store.js +542 -0
- package/dist/server/services/git/haiku.js +2 -2
- package/dist/server/services/plan/agents/check-injection.md +43 -2
- package/dist/server/services/plan/agents/review-code.md +62 -3
- package/dist/server/services/plan/agents/review-quality.md +52 -2
- package/dist/server/services/plan/board-export.js +4 -0
- package/dist/server/services/plan/composer-prompt.js +27 -8
- package/dist/server/services/plan/composer.js +10 -4
- package/dist/server/services/plan/config-installer.js +4 -83
- package/dist/server/services/plan/executor.js +155 -73
- package/dist/server/services/plan/issue-effort.js +9 -21
- package/dist/server/services/plan/issue-prompt-builder.js +72 -9
- package/dist/server/services/plan/parser-core.js +6 -1
- package/dist/server/services/plan/quality-delta.js +6 -24
- package/dist/server/services/plan/readiness-planner.js +12 -4
- package/dist/server/services/plan/record-file.js +52 -0
- package/dist/server/services/plan/review-approval.js +64 -0
- package/dist/server/services/plan/review-gate.js +186 -77
- package/dist/server/services/plan/review-outcome.js +146 -0
- package/dist/server/services/plan/review-report.js +28 -1
- package/dist/server/services/plan/review-target.js +57 -0
- package/dist/server/services/plan/state-reconciler.js +3 -3
- package/dist/server/services/plan/template-diff.js +2 -0
- package/dist/server/services/plan/template-instantiator.js +4 -0
- package/dist/server/services/plan/watcher.js +11 -1
- package/dist/server/services/platform-reconnect.js +7 -0
- package/dist/server/services/platform-token-lifecycle.js +2 -1
- package/dist/server/services/platform.js +293 -50
- package/dist/server/services/relay-http-client.js +107 -0
- package/dist/server/services/schedule/agent-schedule-store.js +69 -8
- package/dist/server/services/schedule/agent-scheduler.js +48 -7
- package/dist/server/services/schedule/daily-timing.js +52 -0
- package/dist/server/services/schedule/prompt-file.js +55 -0
- package/dist/server/services/schedule/run-bootstrap.js +20 -5
- package/dist/server/services/schedule/schedule-store.js +54 -4
- package/dist/server/services/schedule/schedule-wire.js +17 -0
- package/dist/server/services/schedule/scheduler.js +43 -9
- package/dist/server/services/sdk/agent-schedule.js +294 -0
- package/dist/server/services/sdk/agents.js +95 -13
- package/dist/server/services/sdk/app-tools.js +44 -7
- package/dist/server/services/sdk/atomic-write.js +75 -0
- package/dist/server/services/sdk/browser.js +5 -1
- package/dist/server/services/sdk/composer.js +2 -0
- package/dist/server/services/sdk/file-change-notify.js +60 -13
- package/dist/server/services/sdk/files-app.js +12 -7
- package/dist/server/services/sdk/files-search.js +14 -0
- package/dist/server/services/sdk/files-transfer.js +1 -1
- package/dist/server/services/sdk/files-watch.js +721 -0
- package/dist/server/services/sdk/files-workspace.js +37 -11
- package/dist/server/services/sdk/inference-claude.js +4 -2
- package/dist/server/services/sdk/inference.js +16 -2
- package/dist/server/services/sdk/pm-chain.js +0 -0
- package/dist/server/services/sdk/pm-schedule.js +72 -15
- package/dist/server/services/sdk/pm.js +22 -0
- package/dist/server/services/sdk/rate-limits.js +32 -0
- package/dist/server/services/sdk/registry.js +40 -2
- package/dist/server/services/sdk/terminal.js +1 -1
- package/dist/server/services/sentry.js +5 -1
- package/dist/server/services/terminal/pty-manager.js +26 -11
- package/dist/server/services/terminal/pty-utils.js +12 -1
- package/dist/server/services/terminal/terminal-modes.js +87 -0
- package/dist/server/services/timeline/assembler.js +2 -2
- package/dist/server/services/timeline/index.js +1 -1
- package/dist/server/services/websocket/agent-schedule-handlers.js +62 -4
- package/dist/server/services/websocket/ask-user-question-bridge.js +2 -2
- package/dist/server/services/websocket/browser-agent-runtime.js +3 -1
- package/dist/server/services/websocket/browser-handlers.js +5 -2
- package/dist/server/services/websocket/browser-viewer-lifecycle.js +8 -0
- package/dist/server/services/websocket/file-explorer-handlers.js +17 -0
- package/dist/server/services/websocket/file-transfer-http.js +12 -6
- package/dist/server/services/websocket/handler.js +185 -33
- package/dist/server/services/websocket/msg-id-tracker.js +94 -19
- package/dist/server/services/websocket/plan-execution-handlers.js +33 -5
- package/dist/server/services/websocket/plan-handlers.js +10 -5
- package/dist/server/services/websocket/plan-helpers.js +23 -0
- package/dist/server/services/websocket/plan-issue-handlers.js +161 -4
- package/dist/server/services/websocket/plan-sprint-handlers.js +3 -2
- package/dist/server/services/websocket/schedule-handlers.js +8 -2
- package/dist/server/services/websocket/sdk-agent-host.js +5 -1
- package/dist/server/services/websocket/sdk-agent-schedule-host.js +30 -0
- package/dist/server/services/websocket/sdk-browser-host.js +11 -4
- package/dist/server/services/websocket/sdk-handlers.js +6 -1
- package/dist/server/services/websocket/sdk-pm-chain-host.js +20 -0
- package/dist/server/services/websocket/sdk-pm-execution-host.js +21 -14
- package/dist/server/services/websocket/sdk-pm-schedule-host.js +9 -0
- package/dist/server/services/websocket/sdk-terminal-host.js +7 -0
- package/dist/server/services/websocket/settings-handlers.js +6 -18
- package/dist/server/services/websocket/tab-broadcast.js +5 -1
- package/dist/server/services/websocket/terminal-handlers.js +51 -6
- package/dist/server/services/websocket/types.js +6 -5
- package/dist/server/services/websocket/viewer-interest.js +30 -0
- package/package.json +6 -6
|
@@ -5,10 +5,12 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
5
5
|
import { writeFileAtomic } from '../../utils/file-store.js';
|
|
6
6
|
import { spacePath } from '../space-layout.js';
|
|
7
7
|
import { isEngineId } from '../websocket/types.js';
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import { nextWallClockFireAt, normalizeWallClockTiming } from './daily-timing.js';
|
|
9
|
+
import { isValidPromptFilePath } from './prompt-file.js';
|
|
10
|
+
import { computeNextFireAt, exceedsMaxLateness, isValidMaxLatenessMs, MAX_SCHEDULE_HISTORY, SCHEDULE_MIN_INTERVAL_MS, SCHEDULE_MIN_LATENESS_MS, SCHEDULE_MISSED_LATENESS_ERROR, } from './schedule-store.js';
|
|
11
|
+
export { computeNextFireAt, exceedsMaxLateness, isValidMaxLatenessMs, MAX_SCHEDULE_HISTORY, SCHEDULE_MIN_INTERVAL_MS, SCHEDULE_MIN_LATENESS_MS, SCHEDULE_MISSED_LATENESS_ERROR, };
|
|
10
12
|
export function isAgentScheduleTiming(timing) {
|
|
11
|
-
return timing.kind
|
|
13
|
+
return timing.kind !== 'continuous';
|
|
12
14
|
}
|
|
13
15
|
export function agentScheduleStorePath(workingDir) {
|
|
14
16
|
return spacePath(workingDir, 'agents', 'schedules.json');
|
|
@@ -20,6 +22,9 @@ function clampTiming(timing) {
|
|
|
20
22
|
if (timing.kind === 'interval') {
|
|
21
23
|
return { kind: 'interval', everyMs: Math.max(timing.everyMs, SCHEDULE_MIN_INTERVAL_MS) };
|
|
22
24
|
}
|
|
25
|
+
if (timing.kind === 'daily' || timing.kind === 'monthly') {
|
|
26
|
+
return normalizeWallClockTiming(timing) ?? timing;
|
|
27
|
+
}
|
|
23
28
|
return timing;
|
|
24
29
|
}
|
|
25
30
|
function isScheduleFireStatus(value) {
|
|
@@ -43,6 +48,16 @@ function normalizeTiming(raw) {
|
|
|
43
48
|
return null;
|
|
44
49
|
return clampTiming({ kind: 'interval', everyMs: t.everyMs });
|
|
45
50
|
}
|
|
51
|
+
if (t.kind === 'daily' || t.kind === 'monthly') {
|
|
52
|
+
return normalizeWallClockTiming(raw);
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function normalizeNextFireAt(raw, timing, now) {
|
|
57
|
+
if (typeof raw === 'string')
|
|
58
|
+
return raw;
|
|
59
|
+
if (timing.kind === 'daily' || timing.kind === 'monthly')
|
|
60
|
+
return nextWallClockFireAt(timing, now);
|
|
46
61
|
return null;
|
|
47
62
|
}
|
|
48
63
|
function normalizeEngineOverride(raw) {
|
|
@@ -96,7 +111,20 @@ function normalizeLastFire(raw) {
|
|
|
96
111
|
return undefined;
|
|
97
112
|
return { at: l.at, status: l.status, tabId: l.tabId };
|
|
98
113
|
}
|
|
99
|
-
function
|
|
114
|
+
function normalizePromptFile(raw) {
|
|
115
|
+
return typeof raw === 'string' && isValidPromptFilePath(raw) ? raw : undefined;
|
|
116
|
+
}
|
|
117
|
+
function normalizeInitiator(raw) {
|
|
118
|
+
if (!raw || typeof raw !== 'object')
|
|
119
|
+
return undefined;
|
|
120
|
+
const i = raw;
|
|
121
|
+
if (typeof i.appId !== 'string' || !i.appId)
|
|
122
|
+
return undefined;
|
|
123
|
+
if (typeof i.origin !== 'string' || !i.origin)
|
|
124
|
+
return undefined;
|
|
125
|
+
return { appId: i.appId, origin: i.origin };
|
|
126
|
+
}
|
|
127
|
+
function normalizeAgentScheduleRecord(raw, now) {
|
|
100
128
|
if (!raw || typeof raw !== 'object')
|
|
101
129
|
return null;
|
|
102
130
|
const r = raw;
|
|
@@ -107,21 +135,27 @@ function normalizeAgentScheduleRecord(raw) {
|
|
|
107
135
|
const timing = normalizeTiming(r.timing);
|
|
108
136
|
if (!timing)
|
|
109
137
|
return null;
|
|
138
|
+
const maxLatenessMs = r.maxLatenessMs;
|
|
139
|
+
if (maxLatenessMs !== undefined && !isValidMaxLatenessMs(maxLatenessMs))
|
|
140
|
+
return null;
|
|
110
141
|
const history = Array.isArray(r.history)
|
|
111
142
|
? r.history.map(normalizeHistoryEntry).filter((e) => e !== null)
|
|
112
143
|
: [];
|
|
113
144
|
return {
|
|
114
145
|
...r,
|
|
115
146
|
id: r.id,
|
|
116
|
-
createdAt: typeof r.createdAt === 'string' ? r.createdAt :
|
|
147
|
+
createdAt: typeof r.createdAt === 'string' ? r.createdAt : now.toISOString(),
|
|
117
148
|
name: typeof r.name === 'string' ? r.name : undefined,
|
|
118
149
|
prompt: r.prompt,
|
|
150
|
+
promptFile: normalizePromptFile(r.promptFile),
|
|
151
|
+
initiator: normalizeInitiator(r.initiator),
|
|
119
152
|
engineOverride: normalizeEngineOverride(r.engineOverride),
|
|
120
153
|
timing,
|
|
121
154
|
enabled: typeof r.enabled === 'boolean' ? r.enabled : true,
|
|
122
|
-
nextFireAt:
|
|
155
|
+
nextFireAt: normalizeNextFireAt(r.nextFireAt, timing, now),
|
|
123
156
|
lastFire: normalizeLastFire(r.lastFire),
|
|
124
157
|
history,
|
|
158
|
+
maxLatenessMs,
|
|
125
159
|
};
|
|
126
160
|
}
|
|
127
161
|
export class AgentScheduleStore {
|
|
@@ -136,8 +170,9 @@ export class AgentScheduleStore {
|
|
|
136
170
|
if (existsSync(this.schedulesPath)) {
|
|
137
171
|
const raw = JSON.parse(readFileSync(this.schedulesPath, 'utf-8'));
|
|
138
172
|
const rawSchedules = Array.isArray(raw?.schedules) ? raw.schedules : [];
|
|
173
|
+
const now = new Date();
|
|
139
174
|
const schedules = rawSchedules
|
|
140
|
-
.map((s) => normalizeAgentScheduleRecord(s))
|
|
175
|
+
.map((s) => normalizeAgentScheduleRecord(s, now))
|
|
141
176
|
.filter((s) => s !== null);
|
|
142
177
|
return { version: 1, schedules };
|
|
143
178
|
}
|
|
@@ -170,11 +205,14 @@ export class AgentScheduleStore {
|
|
|
170
205
|
createdAt: now.toISOString(),
|
|
171
206
|
name: fields.name,
|
|
172
207
|
prompt: fields.prompt,
|
|
208
|
+
...(fields.promptFile !== undefined ? { promptFile: fields.promptFile } : {}),
|
|
209
|
+
...(fields.initiator !== undefined ? { initiator: fields.initiator } : {}),
|
|
173
210
|
engineOverride: fields.engineOverride,
|
|
174
211
|
timing,
|
|
175
212
|
enabled: fields.enabled ?? true,
|
|
176
213
|
nextFireAt: computeNextFireAt(timing, now),
|
|
177
214
|
history: [],
|
|
215
|
+
...(fields.maxLatenessMs !== undefined ? { maxLatenessMs: fields.maxLatenessMs } : {}),
|
|
178
216
|
};
|
|
179
217
|
this.data.schedules.push(record);
|
|
180
218
|
this.save();
|
|
@@ -186,13 +224,20 @@ export class AgentScheduleStore {
|
|
|
186
224
|
return undefined;
|
|
187
225
|
if (patch.name !== undefined)
|
|
188
226
|
record.name = patch.name;
|
|
189
|
-
if (patch.prompt !== undefined)
|
|
227
|
+
if (patch.prompt !== undefined) {
|
|
190
228
|
record.prompt = patch.prompt;
|
|
229
|
+
record.promptFile = undefined;
|
|
230
|
+
}
|
|
231
|
+
if (patch.promptFile !== undefined)
|
|
232
|
+
record.promptFile = patch.promptFile;
|
|
191
233
|
if (patch.engineOverride !== undefined) {
|
|
192
234
|
record.engineOverride = patch.engineOverride === null ? undefined : patch.engineOverride;
|
|
193
235
|
}
|
|
194
236
|
if (patch.enabled !== undefined)
|
|
195
237
|
record.enabled = patch.enabled;
|
|
238
|
+
if (patch.maxLatenessMs !== undefined) {
|
|
239
|
+
record.maxLatenessMs = patch.maxLatenessMs === null ? undefined : patch.maxLatenessMs;
|
|
240
|
+
}
|
|
196
241
|
if (patch.timing !== undefined) {
|
|
197
242
|
record.timing = clampTiming(patch.timing);
|
|
198
243
|
record.nextFireAt = computeNextFireAt(record.timing, new Date());
|
|
@@ -216,6 +261,22 @@ export class AgentScheduleStore {
|
|
|
216
261
|
this.save();
|
|
217
262
|
return { ...record };
|
|
218
263
|
}
|
|
264
|
+
refreshPromptFromFile(id, content) {
|
|
265
|
+
const record = this.data.schedules.find((s) => s.id === id);
|
|
266
|
+
if (!record)
|
|
267
|
+
return undefined;
|
|
268
|
+
record.prompt = content;
|
|
269
|
+
this.save();
|
|
270
|
+
return { ...record };
|
|
271
|
+
}
|
|
272
|
+
restoreNextFireAt(id, nextFireAt) {
|
|
273
|
+
const record = this.data.schedules.find((s) => s.id === id);
|
|
274
|
+
if (!record)
|
|
275
|
+
return undefined;
|
|
276
|
+
record.nextFireAt = nextFireAt;
|
|
277
|
+
this.save();
|
|
278
|
+
return { ...record };
|
|
279
|
+
}
|
|
219
280
|
recordFire(id, entry, nextFireAt) {
|
|
220
281
|
const record = this.data.schedules.find((s) => s.id === id);
|
|
221
282
|
if (!record)
|
|
@@ -4,7 +4,8 @@ import { ImprovisationSessionManager } from '../../cli/improvisation-session-man
|
|
|
4
4
|
import { ENGINE_EFFORT_LEVELS, engineForModel } from '../../engines/types.js';
|
|
5
5
|
import { getEffortLevel, getEngine, getFastMode, getModel } from '../settings.js';
|
|
6
6
|
import { resolveEngineForSession, runAcceptedExecute, setupSessionListeners } from '../websocket/session-handlers.js';
|
|
7
|
-
import { AgentScheduleStore } from './agent-schedule-store.js';
|
|
7
|
+
import { AgentScheduleStore, computeNextFireAt, exceedsMaxLateness, SCHEDULE_MISSED_LATENESS_ERROR, } from './agent-schedule-store.js';
|
|
8
|
+
import { readPromptFile } from './prompt-file.js';
|
|
8
9
|
import { SCHEDULE_ONCE_GRACE_MS } from './scheduler.js';
|
|
9
10
|
export const AGENT_SCHEDULER_TICK_MS = 30_000;
|
|
10
11
|
export { SCHEDULE_ONCE_GRACE_MS };
|
|
@@ -74,6 +75,26 @@ export class AgentSchedulerEngine {
|
|
|
74
75
|
this.runs.reset();
|
|
75
76
|
this.store = null;
|
|
76
77
|
}
|
|
78
|
+
fireNow(scheduleId) {
|
|
79
|
+
const store = this.store;
|
|
80
|
+
if (!store)
|
|
81
|
+
return { kind: 'not-found' };
|
|
82
|
+
this.reapFinishedRuns();
|
|
83
|
+
const before = store.get(scheduleId);
|
|
84
|
+
if (!before)
|
|
85
|
+
return { kind: 'not-found' };
|
|
86
|
+
if (!before.enabled)
|
|
87
|
+
return { kind: 'disabled' };
|
|
88
|
+
const plannedNextFireAt = before.nextFireAt;
|
|
89
|
+
if (this.runs.has(scheduleId)) {
|
|
90
|
+
this.recordSkippedOverlap(before, Date.now());
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
this.fire(before, Date.now());
|
|
94
|
+
}
|
|
95
|
+
store.restoreNextFireAt(scheduleId, plannedNextFireAt);
|
|
96
|
+
return { kind: 'ok', record: store.get(scheduleId) };
|
|
97
|
+
}
|
|
77
98
|
tick() {
|
|
78
99
|
const store = this.store;
|
|
79
100
|
if (!store)
|
|
@@ -94,6 +115,10 @@ export class AgentSchedulerEngine {
|
|
|
94
115
|
}
|
|
95
116
|
if (!this.isDue(record, now))
|
|
96
117
|
continue;
|
|
118
|
+
if (exceedsMaxLateness(record, now)) {
|
|
119
|
+
this.recordMissedLate(record, now);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
97
122
|
this.fire(record, now);
|
|
98
123
|
}
|
|
99
124
|
}
|
|
@@ -140,9 +165,18 @@ export class AgentSchedulerEngine {
|
|
|
140
165
|
if (!store)
|
|
141
166
|
return;
|
|
142
167
|
const firedAt = new Date(now).toISOString();
|
|
168
|
+
let runRecord = record;
|
|
169
|
+
if (record.promptFile) {
|
|
170
|
+
const result = readPromptFile(this.workingDir, record.promptFile);
|
|
171
|
+
if (!result.ok) {
|
|
172
|
+
this.recordFailedFire(record, firedAt, result.reason);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
runRecord = store.refreshPromptFromFile(record.id, result.content) ?? { ...record, prompt: result.content };
|
|
176
|
+
}
|
|
143
177
|
let handle;
|
|
144
178
|
try {
|
|
145
|
-
handle = (this.deps.launch ?? launchScheduledAgentRun)(this.ctx, this.workingDir,
|
|
179
|
+
handle = (this.deps.launch ?? launchScheduledAgentRun)(this.ctx, this.workingDir, runRecord);
|
|
146
180
|
}
|
|
147
181
|
catch (error) {
|
|
148
182
|
this.recordFailedFire(record, firedAt, errorMessage(error));
|
|
@@ -203,20 +237,27 @@ export class AgentSchedulerEngine {
|
|
|
203
237
|
}, this.nextFireAtAfterStart(record, now));
|
|
204
238
|
this.broadcastSchedules();
|
|
205
239
|
}
|
|
240
|
+
recordMissedLate(record, now) {
|
|
241
|
+
const store = this.store;
|
|
242
|
+
if (!store)
|
|
243
|
+
return;
|
|
244
|
+
store.recordFire(record.id, { at: new Date(now).toISOString(), status: 'missed', tabId: null, error: SCHEDULE_MISSED_LATENESS_ERROR }, this.nextFireAtAfterStart(record, now));
|
|
245
|
+
this.broadcastSchedules();
|
|
246
|
+
}
|
|
206
247
|
nextFireAtAfterStart(record, now) {
|
|
207
|
-
if (record.timing.kind === '
|
|
208
|
-
return
|
|
209
|
-
return
|
|
248
|
+
if (record.timing.kind === 'once')
|
|
249
|
+
return null;
|
|
250
|
+
return computeNextFireAt(record.timing, new Date(now));
|
|
210
251
|
}
|
|
211
252
|
nextFireAtAfterFailure(record, now) {
|
|
212
253
|
if (record.timing.kind === 'once')
|
|
213
254
|
return null;
|
|
214
|
-
return new Date(now
|
|
255
|
+
return computeNextFireAt(record.timing, new Date(now));
|
|
215
256
|
}
|
|
216
257
|
nextFireAtAfterSettle(record, now) {
|
|
217
258
|
if (record.timing.kind === 'once')
|
|
218
259
|
return null;
|
|
219
|
-
return this.store?.get(record.id)?.nextFireAt ?? new Date(now
|
|
260
|
+
return this.store?.get(record.id)?.nextFireAt ?? computeNextFireAt(record.timing, new Date(now));
|
|
220
261
|
}
|
|
221
262
|
broadcastSchedules() {
|
|
222
263
|
if (!this.store)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const TIME_PATTERN = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
4
4
|
const WEEKDAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
5
5
|
const CANDIDATE_DAYS = 8;
|
|
6
|
+
const CANDIDATE_MONTHS = 13;
|
|
6
7
|
const formatterCache = new Map();
|
|
7
8
|
function zoneFormatter(timeZone) {
|
|
8
9
|
let formatter = formatterCache.get(timeZone);
|
|
@@ -61,6 +62,32 @@ export function normalizeDailyTiming(raw) {
|
|
|
61
62
|
timing.days = [...new Set(raw.days)].sort((a, b) => a - b);
|
|
62
63
|
return timing;
|
|
63
64
|
}
|
|
65
|
+
function isDayOfMonth(raw) {
|
|
66
|
+
return raw === 'last' || (Number.isInteger(raw) && raw >= 1 && raw <= 31);
|
|
67
|
+
}
|
|
68
|
+
export function isValidMonthlyTiming(raw) {
|
|
69
|
+
if (!raw || typeof raw !== 'object')
|
|
70
|
+
return false;
|
|
71
|
+
const t = raw;
|
|
72
|
+
if (t.kind !== 'monthly')
|
|
73
|
+
return false;
|
|
74
|
+
if (typeof t.time !== 'string' || !TIME_PATTERN.test(t.time))
|
|
75
|
+
return false;
|
|
76
|
+
if (!isValidTimeZone(t.timeZone))
|
|
77
|
+
return false;
|
|
78
|
+
return isDayOfMonth(t.dayOfMonth);
|
|
79
|
+
}
|
|
80
|
+
export function normalizeMonthlyTiming(raw) {
|
|
81
|
+
if (!isValidMonthlyTiming(raw))
|
|
82
|
+
return null;
|
|
83
|
+
return { kind: 'monthly', time: raw.time, timeZone: raw.timeZone, dayOfMonth: raw.dayOfMonth };
|
|
84
|
+
}
|
|
85
|
+
export function isValidWallClockTiming(raw) {
|
|
86
|
+
return isValidDailyTiming(raw) || isValidMonthlyTiming(raw);
|
|
87
|
+
}
|
|
88
|
+
export function normalizeWallClockTiming(raw) {
|
|
89
|
+
return normalizeDailyTiming(raw) ?? normalizeMonthlyTiming(raw);
|
|
90
|
+
}
|
|
64
91
|
export function zonedParts(instant, timeZone) {
|
|
65
92
|
const parts = zoneFormatter(timeZone).formatToParts(new Date(instant));
|
|
66
93
|
const read = (type) => parts.find((part) => part.type === type)?.value ?? '0';
|
|
@@ -103,3 +130,28 @@ export function nextDailyFireAt(timing, from) {
|
|
|
103
130
|
}
|
|
104
131
|
throw new Error(`no daily slot within ${CANDIDATE_DAYS} days for ${timing.time} in ${timing.timeZone}`);
|
|
105
132
|
}
|
|
133
|
+
function daysInMonth(year, month) {
|
|
134
|
+
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
135
|
+
}
|
|
136
|
+
export function nextMonthlyFireAt(timing, from) {
|
|
137
|
+
const [hour, minute] = timing.time.split(':').map(Number);
|
|
138
|
+
const fromMs = from.getTime();
|
|
139
|
+
const today = zonedParts(fromMs, timing.timeZone);
|
|
140
|
+
for (let offset = 0; offset < CANDIDATE_MONTHS; offset++) {
|
|
141
|
+
const monthIndex = today.month - 1 + offset;
|
|
142
|
+
const year = today.year + Math.floor(monthIndex / 12);
|
|
143
|
+
const month = (monthIndex % 12) + 1;
|
|
144
|
+
const lastDay = daysInMonth(year, month);
|
|
145
|
+
const day = timing.dayOfMonth === 'last' ? lastDay : timing.dayOfMonth;
|
|
146
|
+
if (day > lastDay)
|
|
147
|
+
continue;
|
|
148
|
+
const candidate = instantForWallTime(year, month, day, hour, minute, timing.timeZone);
|
|
149
|
+
if (candidate <= fromMs)
|
|
150
|
+
continue;
|
|
151
|
+
return new Date(candidate).toISOString();
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`no monthly slot within ${CANDIDATE_MONTHS} months for day ${timing.dayOfMonth} at ${timing.time} in ${timing.timeZone}`);
|
|
154
|
+
}
|
|
155
|
+
export function nextWallClockFireAt(timing, from) {
|
|
156
|
+
return timing.kind === 'daily' ? nextDailyFireAt(timing, from) : nextMonthlyFireAt(timing, from);
|
|
157
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
|
+
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import { guardWorkspacePath } from '../sdk/files-workspace.js';
|
|
5
|
+
export const PROMPT_FILE_MAX_BYTES = 262_144;
|
|
6
|
+
export function isValidPromptFilePath(path) {
|
|
7
|
+
if (typeof path !== 'string' || path.length === 0 || path.startsWith('/'))
|
|
8
|
+
return false;
|
|
9
|
+
return path.split('/').every((segment) => segment !== '..');
|
|
10
|
+
}
|
|
11
|
+
export function readPromptFile(workingDir, promptFile) {
|
|
12
|
+
if (!isValidPromptFilePath(promptFile)) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
reason: `promptFile '${promptFile}' must be a Space-relative POSIX path with no leading '/' and no '..' segment`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
let resolvedPath;
|
|
19
|
+
try {
|
|
20
|
+
;
|
|
21
|
+
({ resolvedPath } = guardWorkspacePath(workingDir, promptFile));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
25
|
+
}
|
|
26
|
+
let stat;
|
|
27
|
+
try {
|
|
28
|
+
stat = statSync(resolvedPath);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { ok: false, reason: `promptFile '${promptFile}' does not exist` };
|
|
32
|
+
}
|
|
33
|
+
if (stat.isDirectory()) {
|
|
34
|
+
return { ok: false, reason: `promptFile '${promptFile}' is a directory, not a file` };
|
|
35
|
+
}
|
|
36
|
+
if (stat.size > PROMPT_FILE_MAX_BYTES) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
reason: `promptFile '${promptFile}' is ${stat.size} bytes — exceeds the ${PROMPT_FILE_MAX_BYTES}-byte limit`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const buffer = readFileSync(resolvedPath);
|
|
43
|
+
let content;
|
|
44
|
+
try {
|
|
45
|
+
content = new TextDecoder('utf-8', { fatal: true }).decode(buffer);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { ok: false, reason: `promptFile '${promptFile}' is not valid UTF-8` };
|
|
49
|
+
}
|
|
50
|
+
const trimmed = content.trim();
|
|
51
|
+
if (trimmed.length === 0) {
|
|
52
|
+
return { ok: false, reason: `promptFile '${promptFile}' is empty after trimming` };
|
|
53
|
+
}
|
|
54
|
+
return { ok: true, content: trimmed };
|
|
55
|
+
}
|
|
@@ -123,16 +123,31 @@ function ensureStateFile(boardDir) {
|
|
|
123
123
|
writeFileSync(statePath, skeleton, 'utf-8');
|
|
124
124
|
}
|
|
125
125
|
export function writeRunInputFile(boardDir, options) {
|
|
126
|
+
const outDir = join(boardDir, 'out');
|
|
127
|
+
const path = join(outDir, 'run-input.json');
|
|
128
|
+
let existing = null;
|
|
129
|
+
if (options.upstream && existsSync(path)) {
|
|
130
|
+
let parsed;
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
138
|
+
return null;
|
|
139
|
+
existing = parsed;
|
|
140
|
+
}
|
|
141
|
+
const scheduleId = options.scheduleId ?? existing?.scheduleId;
|
|
126
142
|
const file = {
|
|
127
143
|
schema: RUN_INPUT_SCHEMA,
|
|
128
|
-
runAt: options.runAt ?? new Date().toISOString(),
|
|
144
|
+
runAt: options.runAt ?? existing?.runAt ?? new Date().toISOString(),
|
|
129
145
|
boardId: options.boardId,
|
|
130
|
-
...(
|
|
131
|
-
data: options.data,
|
|
146
|
+
...(scheduleId ? { scheduleId } : {}),
|
|
147
|
+
data: options.data !== undefined ? options.data : existing && 'data' in existing ? existing.data : null,
|
|
148
|
+
...(options.upstream ? { upstream: options.upstream } : {}),
|
|
132
149
|
};
|
|
133
|
-
const outDir = join(boardDir, 'out');
|
|
134
150
|
mkdirSync(outDir, { recursive: true });
|
|
135
|
-
const path = join(outDir, 'run-input.json');
|
|
136
151
|
writeFileSync(path, JSON.stringify(file, null, 2), 'utf-8');
|
|
137
152
|
return path;
|
|
138
153
|
}
|
|
@@ -4,8 +4,14 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { defaultPmDir } from '../plan/parser.js';
|
|
7
|
+
import { nextWallClockFireAt, normalizeWallClockTiming } from './daily-timing.js';
|
|
8
|
+
export function isRecurringTiming(timing) {
|
|
9
|
+
return timing.kind !== 'once' && timing.kind !== 'continuous';
|
|
10
|
+
}
|
|
7
11
|
export const SCHEDULE_MIN_INTERVAL_MS = 60_000;
|
|
8
12
|
export const MAX_SCHEDULE_HISTORY = 20;
|
|
13
|
+
export const SCHEDULE_MIN_LATENESS_MS = 60_000;
|
|
14
|
+
export const SCHEDULE_MISSED_LATENESS_ERROR = 'missed by more than the allowed lateness';
|
|
9
15
|
function generateScheduleId() {
|
|
10
16
|
return `sched-${randomUUID().replace(/-/g, '').slice(0, 8)}`;
|
|
11
17
|
}
|
|
@@ -13,8 +19,14 @@ function clampTiming(timing) {
|
|
|
13
19
|
if (timing.kind === 'interval') {
|
|
14
20
|
return { kind: 'interval', everyMs: Math.max(timing.everyMs, SCHEDULE_MIN_INTERVAL_MS) };
|
|
15
21
|
}
|
|
22
|
+
if (timing.kind === 'daily' || timing.kind === 'monthly') {
|
|
23
|
+
return normalizeWallClockTiming(timing) ?? timing;
|
|
24
|
+
}
|
|
16
25
|
return timing;
|
|
17
26
|
}
|
|
27
|
+
export function isValidMaxLatenessMs(value) {
|
|
28
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= SCHEDULE_MIN_LATENESS_MS;
|
|
29
|
+
}
|
|
18
30
|
export function computeNextFireAt(timing, from) {
|
|
19
31
|
if (timing.kind === 'once') {
|
|
20
32
|
const at = new Date(timing.at);
|
|
@@ -26,8 +38,19 @@ export function computeNextFireAt(timing, from) {
|
|
|
26
38
|
const everyMs = Math.max(timing.everyMs, SCHEDULE_MIN_INTERVAL_MS);
|
|
27
39
|
return new Date(from.getTime() + everyMs).toISOString();
|
|
28
40
|
}
|
|
41
|
+
if (timing.kind === 'daily' || timing.kind === 'monthly') {
|
|
42
|
+
return nextWallClockFireAt(timing, from);
|
|
43
|
+
}
|
|
29
44
|
return null;
|
|
30
45
|
}
|
|
46
|
+
export function exceedsMaxLateness(record, now) {
|
|
47
|
+
if (record.maxLatenessMs === undefined || !isRecurringTiming(record.timing))
|
|
48
|
+
return false;
|
|
49
|
+
if (!record.nextFireAt)
|
|
50
|
+
return false;
|
|
51
|
+
const planned = Date.parse(record.nextFireAt);
|
|
52
|
+
return Number.isFinite(planned) && now - planned > record.maxLatenessMs;
|
|
53
|
+
}
|
|
31
54
|
function isScheduleFireStatus(value) {
|
|
32
55
|
return value === 'started' || value === 'completed' || value === 'failed' || value === 'skipped-overlap' || value === 'missed';
|
|
33
56
|
}
|
|
@@ -61,6 +84,9 @@ function normalizeTiming(raw) {
|
|
|
61
84
|
if (t.kind === 'continuous') {
|
|
62
85
|
return { kind: 'continuous' };
|
|
63
86
|
}
|
|
87
|
+
if (t.kind === 'daily' || t.kind === 'monthly') {
|
|
88
|
+
return normalizeWallClockTiming(raw);
|
|
89
|
+
}
|
|
64
90
|
return null;
|
|
65
91
|
}
|
|
66
92
|
function normalizeInput(raw) {
|
|
@@ -95,7 +121,14 @@ function normalizeLastFire(raw) {
|
|
|
95
121
|
return undefined;
|
|
96
122
|
return { at: l.at, status: l.status, boardId: l.boardId };
|
|
97
123
|
}
|
|
98
|
-
function
|
|
124
|
+
function normalizeNextFireAt(raw, timing, now) {
|
|
125
|
+
if (typeof raw === 'string')
|
|
126
|
+
return raw;
|
|
127
|
+
if (timing.kind === 'daily' || timing.kind === 'monthly')
|
|
128
|
+
return nextWallClockFireAt(timing, now);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
function normalizeScheduleRecord(raw, now) {
|
|
99
132
|
if (!raw || typeof raw !== 'object')
|
|
100
133
|
return null;
|
|
101
134
|
const r = raw;
|
|
@@ -105,22 +138,26 @@ function normalizeScheduleRecord(raw) {
|
|
|
105
138
|
const timing = normalizeTiming(r.timing);
|
|
106
139
|
if (!source || !timing)
|
|
107
140
|
return null;
|
|
141
|
+
const maxLatenessMs = r.maxLatenessMs;
|
|
142
|
+
if (maxLatenessMs !== undefined && !isValidMaxLatenessMs(maxLatenessMs))
|
|
143
|
+
return null;
|
|
108
144
|
const history = Array.isArray(r.history)
|
|
109
145
|
? (r.history.map(normalizeHistoryEntry).filter((e) => e !== null))
|
|
110
146
|
: [];
|
|
111
147
|
return {
|
|
112
148
|
...r,
|
|
113
149
|
id: r.id,
|
|
114
|
-
createdAt: typeof r.createdAt === 'string' ? r.createdAt :
|
|
150
|
+
createdAt: typeof r.createdAt === 'string' ? r.createdAt : now.toISOString(),
|
|
115
151
|
name: typeof r.name === 'string' ? r.name : undefined,
|
|
116
152
|
source,
|
|
117
153
|
input: normalizeInput(r.input),
|
|
118
154
|
timing,
|
|
119
155
|
enabled: typeof r.enabled === 'boolean' ? r.enabled : true,
|
|
120
|
-
nextFireAt:
|
|
156
|
+
nextFireAt: normalizeNextFireAt(r.nextFireAt, timing, now),
|
|
121
157
|
lastFire: normalizeLastFire(r.lastFire),
|
|
122
158
|
history,
|
|
123
159
|
consecutiveEmptyRuns: typeof r.consecutiveEmptyRuns === 'number' ? r.consecutiveEmptyRuns : 0,
|
|
160
|
+
maxLatenessMs,
|
|
124
161
|
};
|
|
125
162
|
}
|
|
126
163
|
export class ScheduleStore {
|
|
@@ -142,8 +179,9 @@ export class ScheduleStore {
|
|
|
142
179
|
if (existsSync(this.schedulesPath)) {
|
|
143
180
|
const raw = JSON.parse(readFileSync(this.schedulesPath, 'utf-8'));
|
|
144
181
|
const rawSchedules = Array.isArray(raw?.schedules) ? raw.schedules : [];
|
|
182
|
+
const now = new Date();
|
|
145
183
|
const schedules = rawSchedules
|
|
146
|
-
.map((s) => normalizeScheduleRecord(s))
|
|
184
|
+
.map((s) => normalizeScheduleRecord(s, now))
|
|
147
185
|
.filter((s) => s !== null);
|
|
148
186
|
return { version: 1, schedules };
|
|
149
187
|
}
|
|
@@ -182,6 +220,7 @@ export class ScheduleStore {
|
|
|
182
220
|
nextFireAt: computeNextFireAt(timing, now),
|
|
183
221
|
history: [],
|
|
184
222
|
consecutiveEmptyRuns: 0,
|
|
223
|
+
...(fields.maxLatenessMs !== undefined ? { maxLatenessMs: fields.maxLatenessMs } : {}),
|
|
185
224
|
};
|
|
186
225
|
this.data.schedules.push(record);
|
|
187
226
|
this.save();
|
|
@@ -197,6 +236,9 @@ export class ScheduleStore {
|
|
|
197
236
|
record.input = patch.input === null ? undefined : patch.input;
|
|
198
237
|
if (patch.enabled !== undefined)
|
|
199
238
|
record.enabled = patch.enabled;
|
|
239
|
+
if (patch.maxLatenessMs !== undefined) {
|
|
240
|
+
record.maxLatenessMs = patch.maxLatenessMs === null ? undefined : patch.maxLatenessMs;
|
|
241
|
+
}
|
|
200
242
|
if (patch.timing !== undefined) {
|
|
201
243
|
record.timing = clampTiming(patch.timing);
|
|
202
244
|
record.nextFireAt = computeNextFireAt(record.timing, new Date());
|
|
@@ -220,6 +262,14 @@ export class ScheduleStore {
|
|
|
220
262
|
this.save();
|
|
221
263
|
return { ...record };
|
|
222
264
|
}
|
|
265
|
+
restoreNextFireAt(id, nextFireAt) {
|
|
266
|
+
const record = this.data.schedules.find((s) => s.id === id);
|
|
267
|
+
if (!record)
|
|
268
|
+
return undefined;
|
|
269
|
+
record.nextFireAt = nextFireAt;
|
|
270
|
+
this.save();
|
|
271
|
+
return { ...record };
|
|
272
|
+
}
|
|
223
273
|
recordFire(id, lastFire, nextFireAt, consecutiveEmptyRuns) {
|
|
224
274
|
const record = this.data.schedules.find((s) => s.id === id);
|
|
225
275
|
if (!record)
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { basename, dirname, join, relative } from 'node:path';
|
|
6
|
+
import { isValidWallClockTiming } from './daily-timing.js';
|
|
7
|
+
import { isValidMaxLatenessMs } from './schedule-store.js';
|
|
6
8
|
import { resolveTemplateSource, TemplateSourceError } from './template-source.js';
|
|
7
9
|
export function parseCreateSourceWire(raw) {
|
|
8
10
|
if (!raw || typeof raw !== 'object')
|
|
@@ -24,8 +26,15 @@ export function isValidTiming(t) {
|
|
|
24
26
|
return typeof timing.at === 'string' && !Number.isNaN(Date.parse(timing.at));
|
|
25
27
|
if (timing.kind === 'interval')
|
|
26
28
|
return typeof timing.everyMs === 'number' && Number.isFinite(timing.everyMs);
|
|
29
|
+
if (timing.kind === 'daily' || timing.kind === 'monthly')
|
|
30
|
+
return isValidWallClockTiming(t);
|
|
27
31
|
return timing.kind === 'continuous';
|
|
28
32
|
}
|
|
33
|
+
export function parseMaxLatenessMs(raw) {
|
|
34
|
+
if (raw === undefined)
|
|
35
|
+
return { ok: true, value: undefined };
|
|
36
|
+
return isValidMaxLatenessMs(raw) ? { ok: true, value: raw } : { ok: false };
|
|
37
|
+
}
|
|
29
38
|
export function resolveCreateSource(raw, workingDir, pmDir) {
|
|
30
39
|
const retentionKey = randomUUID();
|
|
31
40
|
const retentionDir = join(pmDir, 'schedules', retentionKey);
|
|
@@ -73,5 +82,13 @@ export function parseUpdatePatch(patch) {
|
|
|
73
82
|
return null;
|
|
74
83
|
safePatch.timing = patch.timing;
|
|
75
84
|
}
|
|
85
|
+
if (patch.maxLatenessMs === null) {
|
|
86
|
+
safePatch.maxLatenessMs = null;
|
|
87
|
+
}
|
|
88
|
+
else if (patch.maxLatenessMs !== undefined) {
|
|
89
|
+
if (!isValidMaxLatenessMs(patch.maxLatenessMs))
|
|
90
|
+
return null;
|
|
91
|
+
safePatch.maxLatenessMs = patch.maxLatenessMs;
|
|
92
|
+
}
|
|
76
93
|
return safePatch;
|
|
77
94
|
}
|