@bradygaster/squad-sdk 0.8.24 → 0.9.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/dist/adapter/client.d.ts +17 -0
- package/dist/adapter/client.d.ts.map +1 -1
- package/dist/adapter/client.js +101 -1
- package/dist/adapter/client.js.map +1 -1
- package/dist/agents/history-shadow.d.ts.map +1 -1
- package/dist/agents/history-shadow.js +99 -32
- package/dist/agents/history-shadow.js.map +1 -1
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +2 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/model-selector.d.ts +2 -0
- package/dist/agents/model-selector.d.ts.map +1 -1
- package/dist/agents/model-selector.js +41 -35
- package/dist/agents/model-selector.js.map +1 -1
- package/dist/agents/personal.d.ts +35 -0
- package/dist/agents/personal.d.ts.map +1 -0
- package/dist/agents/personal.js +67 -0
- package/dist/agents/personal.js.map +1 -0
- package/dist/builders/index.d.ts +3 -2
- package/dist/builders/index.d.ts.map +1 -1
- package/dist/builders/index.js +28 -0
- package/dist/builders/index.js.map +1 -1
- package/dist/builders/types.d.ts +13 -0
- package/dist/builders/types.d.ts.map +1 -1
- package/dist/config/init.d.ts +8 -0
- package/dist/config/init.d.ts.map +1 -1
- package/dist/config/init.js +131 -20
- package/dist/config/init.js.map +1 -1
- package/dist/config/models.d.ts +112 -0
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +329 -18
- package/dist/config/models.js.map +1 -1
- package/dist/coordinator/index.js +2 -2
- package/dist/coordinator/index.js.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/platform/azure-devops.d.ts +42 -0
- package/dist/platform/azure-devops.d.ts.map +1 -1
- package/dist/platform/azure-devops.js +75 -0
- package/dist/platform/azure-devops.js.map +1 -1
- package/dist/platform/comms-file-log.d.ts.map +1 -1
- package/dist/platform/comms-file-log.js +2 -1
- package/dist/platform/comms-file-log.js.map +1 -1
- package/dist/platform/index.d.ts +2 -1
- package/dist/platform/index.d.ts.map +1 -1
- package/dist/platform/index.js +1 -0
- package/dist/platform/index.js.map +1 -1
- package/dist/ralph/capabilities.d.ts +67 -0
- package/dist/ralph/capabilities.d.ts.map +1 -0
- package/dist/ralph/capabilities.js +111 -0
- package/dist/ralph/capabilities.js.map +1 -0
- package/dist/ralph/index.d.ts +2 -0
- package/dist/ralph/index.d.ts.map +1 -1
- package/dist/ralph/index.js +6 -5
- package/dist/ralph/index.js.map +1 -1
- package/dist/ralph/rate-limiting.d.ts +99 -0
- package/dist/ralph/rate-limiting.d.ts.map +1 -0
- package/dist/ralph/rate-limiting.js +170 -0
- package/dist/ralph/rate-limiting.js.map +1 -0
- package/dist/resolution.d.ts +24 -2
- package/dist/resolution.d.ts.map +1 -1
- package/dist/resolution.js +106 -6
- package/dist/resolution.js.map +1 -1
- package/dist/roles/catalog-categories.d.ts +146 -0
- package/dist/roles/catalog-categories.d.ts.map +1 -0
- package/dist/roles/catalog-categories.js +374 -0
- package/dist/roles/catalog-categories.js.map +1 -0
- package/dist/roles/catalog-engineering.d.ts +212 -0
- package/dist/roles/catalog-engineering.d.ts.map +1 -0
- package/dist/roles/catalog-engineering.js +549 -0
- package/dist/roles/catalog-engineering.js.map +1 -0
- package/dist/roles/catalog.d.ts +24 -0
- package/dist/roles/catalog.d.ts.map +1 -0
- package/dist/roles/catalog.js +28 -0
- package/dist/roles/catalog.js.map +1 -0
- package/dist/roles/index.d.ts +69 -0
- package/dist/roles/index.d.ts.map +1 -0
- package/dist/roles/index.js +197 -0
- package/dist/roles/index.js.map +1 -0
- package/dist/roles/types.d.ts +87 -0
- package/dist/roles/types.d.ts.map +1 -0
- package/dist/roles/types.js +14 -0
- package/dist/roles/types.js.map +1 -0
- package/dist/runtime/benchmarks.js +5 -5
- package/dist/runtime/benchmarks.js.map +1 -1
- package/dist/runtime/constants.d.ts +2 -2
- package/dist/runtime/constants.d.ts.map +1 -1
- package/dist/runtime/constants.js +5 -3
- package/dist/runtime/constants.js.map +1 -1
- package/dist/runtime/cross-squad.d.ts +118 -0
- package/dist/runtime/cross-squad.d.ts.map +1 -0
- package/dist/runtime/cross-squad.js +234 -0
- package/dist/runtime/cross-squad.js.map +1 -0
- package/dist/runtime/otel-init.d.ts +24 -17
- package/dist/runtime/otel-init.d.ts.map +1 -1
- package/dist/runtime/otel-init.js +29 -20
- package/dist/runtime/otel-init.js.map +1 -1
- package/dist/runtime/otel-metrics.d.ts +5 -0
- package/dist/runtime/otel-metrics.d.ts.map +1 -1
- package/dist/runtime/otel-metrics.js +54 -0
- package/dist/runtime/otel-metrics.js.map +1 -1
- package/dist/runtime/rework.d.ts +71 -0
- package/dist/runtime/rework.d.ts.map +1 -0
- package/dist/runtime/rework.js +107 -0
- package/dist/runtime/rework.js.map +1 -0
- package/dist/runtime/scheduler.d.ts +128 -0
- package/dist/runtime/scheduler.d.ts.map +1 -0
- package/dist/runtime/scheduler.js +427 -0
- package/dist/runtime/scheduler.js.map +1 -0
- package/dist/runtime/squad-observer.d.ts.map +1 -1
- package/dist/runtime/squad-observer.js +4 -0
- package/dist/runtime/squad-observer.js.map +1 -1
- package/dist/runtime/streaming.d.ts +2 -0
- package/dist/runtime/streaming.d.ts.map +1 -1
- package/dist/runtime/streaming.js +6 -0
- package/dist/runtime/streaming.js.map +1 -1
- package/dist/runtime/telemetry.d.ts +2 -0
- package/dist/runtime/telemetry.d.ts.map +1 -1
- package/dist/runtime/telemetry.js +6 -0
- package/dist/runtime/telemetry.js.map +1 -1
- package/dist/sharing/consult.d.ts +2 -2
- package/dist/sharing/consult.js +6 -6
- package/dist/sharing/consult.js.map +1 -1
- package/dist/sharing/export.d.ts.map +1 -1
- package/dist/sharing/export.js +17 -4
- package/dist/sharing/export.js.map +1 -1
- package/dist/skills/handler-types.d.ts +271 -0
- package/dist/skills/handler-types.d.ts.map +1 -0
- package/dist/skills/handler-types.js +31 -0
- package/dist/skills/handler-types.js.map +1 -0
- package/dist/skills/index.d.ts +3 -0
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +3 -0
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/skill-script-loader.d.ts +65 -0
- package/dist/skills/skill-script-loader.d.ts.map +1 -0
- package/dist/skills/skill-script-loader.js +227 -0
- package/dist/skills/skill-script-loader.js.map +1 -0
- package/dist/skills/skill-source.d.ts.map +1 -1
- package/dist/skills/skill-source.js +5 -1
- package/dist/skills/skill-source.js.map +1 -1
- package/dist/tools/index.d.ts +10 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +49 -8
- package/dist/tools/index.js.map +1 -1
- package/dist/upstream/resolver.d.ts.map +1 -1
- package/dist/upstream/resolver.js +14 -5
- package/dist/upstream/resolver.js.map +1 -1
- package/package.json +34 -3
- package/templates/casting/Futurama.json +10 -0
- package/templates/casting-policy.json +4 -2
- package/templates/casting-reference.md +104 -0
- package/templates/cooperative-rate-limiting.md +229 -0
- package/templates/issue-lifecycle.md +412 -0
- package/templates/keda-scaler.md +164 -0
- package/templates/machine-capabilities.md +75 -0
- package/templates/mcp-config.md +0 -8
- package/templates/orchestration-log.md +27 -27
- package/templates/package.json +3 -0
- package/templates/ralph-circuit-breaker.md +313 -0
- package/templates/ralph-triage.js +543 -0
- package/templates/routing.md +5 -20
- package/templates/schedule.json +19 -0
- package/templates/scribe-charter.md +1 -1
- package/templates/skills/agent-collaboration/SKILL.md +42 -0
- package/templates/skills/agent-conduct/SKILL.md +24 -0
- package/templates/skills/architectural-proposals/SKILL.md +151 -0
- package/templates/skills/ci-validation-gates/SKILL.md +84 -0
- package/templates/skills/cli-wiring/SKILL.md +47 -0
- package/templates/skills/client-compatibility/SKILL.md +89 -0
- package/templates/skills/cross-squad/SKILL.md +114 -0
- package/templates/skills/distributed-mesh/SKILL.md +287 -0
- package/templates/skills/distributed-mesh/mesh.json.example +30 -0
- package/templates/skills/distributed-mesh/sync-mesh.ps1 +111 -0
- package/templates/skills/distributed-mesh/sync-mesh.sh +104 -0
- package/templates/skills/docs-standards/SKILL.md +71 -0
- package/templates/skills/economy-mode/SKILL.md +114 -0
- package/templates/skills/external-comms/SKILL.md +329 -0
- package/templates/skills/gh-auth-isolation/SKILL.md +183 -0
- package/templates/skills/git-workflow/SKILL.md +204 -0
- package/templates/skills/github-multi-account/SKILL.md +95 -0
- package/templates/skills/history-hygiene/SKILL.md +36 -0
- package/templates/skills/humanizer/SKILL.md +105 -0
- package/templates/skills/init-mode/SKILL.md +102 -0
- package/templates/skills/model-selection/SKILL.md +117 -0
- package/templates/skills/nap/SKILL.md +24 -0
- package/templates/skills/personal-squad/SKILL.md +57 -0
- package/templates/skills/release-process/SKILL.md +423 -0
- package/templates/skills/reskill/SKILL.md +92 -0
- package/templates/skills/reviewer-protocol/SKILL.md +79 -0
- package/templates/skills/secret-handling/SKILL.md +200 -0
- package/templates/skills/session-recovery/SKILL.md +155 -0
- package/templates/skills/squad-conventions/SKILL.md +69 -0
- package/templates/skills/test-discipline/SKILL.md +37 -0
- package/templates/skills/windows-compatibility/SKILL.md +74 -0
- package/templates/squad.agent.md +1287 -1146
- package/templates/workflows/squad-docs.yml +8 -4
- package/templates/workflows/squad-heartbeat.yml +55 -200
- package/templates/workflows/squad-insider-release.yml +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rework Rate — Pure calculation helpers.
|
|
3
|
+
*
|
|
4
|
+
* Computes PR rework metrics from review and commit data.
|
|
5
|
+
* No I/O, no side effects — safe to import in tests.
|
|
6
|
+
*
|
|
7
|
+
* @module runtime/rework
|
|
8
|
+
*/
|
|
9
|
+
function getCommitDate(c) {
|
|
10
|
+
return c.committedDate ?? c.commit?.committedDate;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Calculate rework metrics for a single PR.
|
|
14
|
+
* Rework = commits pushed after the first review.
|
|
15
|
+
*/
|
|
16
|
+
export function calculatePrRework(pr, reviews, commits) {
|
|
17
|
+
const sortedReviews = reviews
|
|
18
|
+
.filter((r) => !!r.submittedAt)
|
|
19
|
+
.sort((a, b) => new Date(a.submittedAt).getTime() - new Date(b.submittedAt).getTime());
|
|
20
|
+
const sortedCommits = commits
|
|
21
|
+
.filter((c) => !!getCommitDate(c))
|
|
22
|
+
.sort((a, b) => new Date(getCommitDate(a)).getTime() - new Date(getCommitDate(b)).getTime());
|
|
23
|
+
const firstReview = sortedReviews[0];
|
|
24
|
+
const firstReviewTime = firstReview ? new Date(firstReview.submittedAt).getTime() : null;
|
|
25
|
+
// Count review cycles: changes-requested → approved transitions
|
|
26
|
+
let reviewCycles = 0;
|
|
27
|
+
let hadChangesRequested = false;
|
|
28
|
+
let firstChangesRequested = null;
|
|
29
|
+
let lastApproval = null;
|
|
30
|
+
let pendingChangeRequest = false;
|
|
31
|
+
for (const review of sortedReviews) {
|
|
32
|
+
const state = (review.state ?? '').toUpperCase();
|
|
33
|
+
if (state === 'CHANGES_REQUESTED') {
|
|
34
|
+
hadChangesRequested = true;
|
|
35
|
+
pendingChangeRequest = true;
|
|
36
|
+
if (!firstChangesRequested)
|
|
37
|
+
firstChangesRequested = review.submittedAt;
|
|
38
|
+
}
|
|
39
|
+
else if (state === 'APPROVED' && pendingChangeRequest) {
|
|
40
|
+
reviewCycles++;
|
|
41
|
+
pendingChangeRequest = false;
|
|
42
|
+
lastApproval = review.submittedAt;
|
|
43
|
+
}
|
|
44
|
+
else if (state === 'APPROVED') {
|
|
45
|
+
lastApproval = review.submittedAt;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Count post-review commits (rework)
|
|
49
|
+
const postReviewCommits = firstReviewTime
|
|
50
|
+
? sortedCommits.filter((c) => new Date(getCommitDate(c)).getTime() > firstReviewTime)
|
|
51
|
+
: [];
|
|
52
|
+
const totalCommits = sortedCommits.length;
|
|
53
|
+
const reworkCommits = postReviewCommits.length;
|
|
54
|
+
const reworkRate = totalCommits > 0 ? reworkCommits / totalCommits : 0;
|
|
55
|
+
// Rework time: first changes-requested to last approval
|
|
56
|
+
const reworkTimeMs = firstChangesRequested && lastApproval
|
|
57
|
+
? new Date(lastApproval).getTime() - new Date(firstChangesRequested).getTime()
|
|
58
|
+
: null;
|
|
59
|
+
return {
|
|
60
|
+
number: pr.number,
|
|
61
|
+
title: pr.title,
|
|
62
|
+
author: pr.author?.login ?? 'unknown',
|
|
63
|
+
mergedAt: pr.mergedAt,
|
|
64
|
+
totalCommits,
|
|
65
|
+
reworkCommits,
|
|
66
|
+
reworkRate: Math.round(reworkRate * 100),
|
|
67
|
+
reviewCycles,
|
|
68
|
+
hadChangesRequested,
|
|
69
|
+
reworkTimeMs,
|
|
70
|
+
totalReviews: sortedReviews.length,
|
|
71
|
+
additions: pr.additions ?? 0,
|
|
72
|
+
deletions: pr.deletions ?? 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Calculate aggregate rework summary across all analyzed PRs.
|
|
77
|
+
*/
|
|
78
|
+
export function calculateReworkSummary(results) {
|
|
79
|
+
if (results.length === 0)
|
|
80
|
+
return { totalPrs: 0 };
|
|
81
|
+
const totalPrs = results.length;
|
|
82
|
+
const avgReworkRate = Math.round(results.reduce((s, r) => s + r.reworkRate, 0) / totalPrs);
|
|
83
|
+
const prsWithRework = results.filter((r) => r.reworkRate > 0).length;
|
|
84
|
+
const prsWithChangesRequested = results.filter((r) => r.hadChangesRequested).length;
|
|
85
|
+
const avgReviewCycles = +(results.reduce((s, r) => s + r.reviewCycles, 0) / totalPrs).toFixed(1);
|
|
86
|
+
const totalReworkCommits = results.reduce((s, r) => s + r.reworkCommits, 0);
|
|
87
|
+
const totalCommits = results.reduce((s, r) => s + r.totalCommits, 0);
|
|
88
|
+
// Average rework time (only for PRs that had rework time)
|
|
89
|
+
const reworkTimes = results
|
|
90
|
+
.filter((r) => r.reworkTimeMs !== null)
|
|
91
|
+
.map((r) => r.reworkTimeMs);
|
|
92
|
+
const avgReworkTimeHours = reworkTimes.length > 0
|
|
93
|
+
? +(reworkTimes.reduce((s, t) => s + t, 0) / reworkTimes.length / 3_600_000).toFixed(1)
|
|
94
|
+
: null;
|
|
95
|
+
return {
|
|
96
|
+
totalPrs,
|
|
97
|
+
avgReworkRate,
|
|
98
|
+
prsWithRework,
|
|
99
|
+
prsWithChangesRequested,
|
|
100
|
+
avgReviewCycles,
|
|
101
|
+
totalReworkCommits,
|
|
102
|
+
totalCommits,
|
|
103
|
+
avgReworkTimeHours,
|
|
104
|
+
rejectionRate: Math.round((prsWithChangesRequested / totalPrs) * 100),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=rework.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rework.js","sourceRoot":"","sources":["../../src/runtime/rework.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler — Generic, provider-agnostic scheduler for Squad (#296)
|
|
3
|
+
*
|
|
4
|
+
* Unified schedule manifest + provider adapters that replace scattered
|
|
5
|
+
* cron jobs, polling scripts, and manual triggers with a single
|
|
6
|
+
* `.squad/schedule.json` configuration file.
|
|
7
|
+
*
|
|
8
|
+
* Provider model:
|
|
9
|
+
* - LocalPollingProvider — evaluates schedule in ralph-watch loop
|
|
10
|
+
* - GitHubActionsProvider — generates/updates workflow files from schedule
|
|
11
|
+
* - Custom providers via ScheduleProvider interface
|
|
12
|
+
*/
|
|
13
|
+
export interface ScheduleManifest {
|
|
14
|
+
version: number;
|
|
15
|
+
schedules: ScheduleEntry[];
|
|
16
|
+
}
|
|
17
|
+
export interface ScheduleEntry {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
trigger: TriggerConfig;
|
|
22
|
+
task: TaskConfig;
|
|
23
|
+
providers: string[];
|
|
24
|
+
retry?: RetryConfig;
|
|
25
|
+
}
|
|
26
|
+
export type TriggerConfig = CronTrigger | IntervalTrigger | EventTrigger | StartupTrigger;
|
|
27
|
+
export interface CronTrigger {
|
|
28
|
+
type: 'cron';
|
|
29
|
+
cron: string;
|
|
30
|
+
}
|
|
31
|
+
export interface IntervalTrigger {
|
|
32
|
+
type: 'interval';
|
|
33
|
+
intervalSeconds: number;
|
|
34
|
+
}
|
|
35
|
+
export interface EventTrigger {
|
|
36
|
+
type: 'event';
|
|
37
|
+
event: string;
|
|
38
|
+
}
|
|
39
|
+
export interface StartupTrigger {
|
|
40
|
+
type: 'startup';
|
|
41
|
+
}
|
|
42
|
+
export interface TaskConfig {
|
|
43
|
+
type: 'workflow' | 'script' | 'copilot' | 'webhook';
|
|
44
|
+
ref: string;
|
|
45
|
+
args?: Record<string, string>;
|
|
46
|
+
}
|
|
47
|
+
export interface RetryConfig {
|
|
48
|
+
maxRetries: number;
|
|
49
|
+
backoffSeconds: number;
|
|
50
|
+
}
|
|
51
|
+
export interface ScheduleState {
|
|
52
|
+
/** Map of schedule id → last run info */
|
|
53
|
+
runs: Record<string, RunRecord>;
|
|
54
|
+
}
|
|
55
|
+
export interface RunRecord {
|
|
56
|
+
lastRun: string;
|
|
57
|
+
nextDue?: string;
|
|
58
|
+
status: 'success' | 'failure' | 'running';
|
|
59
|
+
error?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface TaskResult {
|
|
62
|
+
success: boolean;
|
|
63
|
+
output?: string;
|
|
64
|
+
error?: string;
|
|
65
|
+
}
|
|
66
|
+
export interface ScheduleProvider {
|
|
67
|
+
readonly name: string;
|
|
68
|
+
execute(entry: ScheduleEntry): Promise<TaskResult>;
|
|
69
|
+
/** Optional: generate platform-native config (e.g. GitHub Actions workflow) */
|
|
70
|
+
generate?(manifest: ScheduleManifest, outDir: string): Promise<string[]>;
|
|
71
|
+
}
|
|
72
|
+
export declare class ScheduleValidationError extends Error {
|
|
73
|
+
constructor(message: string);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Validate a raw object against the ScheduleManifest schema.
|
|
77
|
+
* Throws ScheduleValidationError on invalid input.
|
|
78
|
+
*/
|
|
79
|
+
export declare function validateManifest(data: unknown): ScheduleManifest;
|
|
80
|
+
/**
|
|
81
|
+
* Parse and validate a schedule.json file from disk.
|
|
82
|
+
*/
|
|
83
|
+
export declare function parseSchedule(filePath: string): Promise<ScheduleManifest>;
|
|
84
|
+
/**
|
|
85
|
+
* Evaluate which schedules are due now, based on trigger config and state.
|
|
86
|
+
* Returns a list of entries that should be executed.
|
|
87
|
+
*/
|
|
88
|
+
export declare function evaluateSchedule(manifest: ScheduleManifest, state: ScheduleState, now?: Date): ScheduleEntry[];
|
|
89
|
+
/**
|
|
90
|
+
* Minimal cron evaluation for 5-field cron expressions.
|
|
91
|
+
* Supports: minute hour day-of-month month day-of-week
|
|
92
|
+
* Wildcard (*) and specific values only (no ranges/lists for simplicity).
|
|
93
|
+
*/
|
|
94
|
+
export declare function isCronDue(cron: string, run: RunRecord | undefined, now: Date): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Execute a scheduled task using the specified provider.
|
|
97
|
+
* Includes retry logic if configured.
|
|
98
|
+
*/
|
|
99
|
+
export declare function executeTask(entry: ScheduleEntry, provider: ScheduleProvider): Promise<TaskResult>;
|
|
100
|
+
/**
|
|
101
|
+
* Load schedule state from disk. Returns empty state if file doesn't exist.
|
|
102
|
+
*/
|
|
103
|
+
export declare function loadState(statePath: string): Promise<ScheduleState>;
|
|
104
|
+
/**
|
|
105
|
+
* Save schedule state to disk.
|
|
106
|
+
*/
|
|
107
|
+
export declare function saveState(statePath: string, state: ScheduleState): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* LocalPollingProvider — evaluates schedule in the ralph-watch loop.
|
|
110
|
+
* Executes tasks as local processes or stubs.
|
|
111
|
+
*/
|
|
112
|
+
export declare class LocalPollingProvider implements ScheduleProvider {
|
|
113
|
+
readonly name = "local-polling";
|
|
114
|
+
execute(entry: ScheduleEntry): Promise<TaskResult>;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* GitHubActionsProvider — generates workflow YAML files from schedule manifest.
|
|
118
|
+
*/
|
|
119
|
+
export declare class GitHubActionsProvider implements ScheduleProvider {
|
|
120
|
+
readonly name = "github-actions";
|
|
121
|
+
execute(entry: ScheduleEntry): Promise<TaskResult>;
|
|
122
|
+
generate(manifest: ScheduleManifest, outDir: string): Promise<string[]>;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Default schedule.json template for `squad schedule init`.
|
|
126
|
+
*/
|
|
127
|
+
export declare function defaultScheduleTemplate(): ScheduleManifest;
|
|
128
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/runtime/scheduler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler — Generic, provider-agnostic scheduler for Squad (#296)
|
|
3
|
+
*
|
|
4
|
+
* Unified schedule manifest + provider adapters that replace scattered
|
|
5
|
+
* cron jobs, polling scripts, and manual triggers with a single
|
|
6
|
+
* `.squad/schedule.json` configuration file.
|
|
7
|
+
*
|
|
8
|
+
* Provider model:
|
|
9
|
+
* - LocalPollingProvider — evaluates schedule in ralph-watch loop
|
|
10
|
+
* - GitHubActionsProvider — generates/updates workflow files from schedule
|
|
11
|
+
* - Custom providers via ScheduleProvider interface
|
|
12
|
+
*/
|
|
13
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Validation
|
|
18
|
+
// ============================================================================
|
|
19
|
+
export class ScheduleValidationError extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'ScheduleValidationError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const VALID_TRIGGER_TYPES = ['cron', 'interval', 'event', 'startup'];
|
|
26
|
+
const VALID_TASK_TYPES = ['workflow', 'script', 'copilot', 'webhook'];
|
|
27
|
+
/**
|
|
28
|
+
* Validate a raw object against the ScheduleManifest schema.
|
|
29
|
+
* Throws ScheduleValidationError on invalid input.
|
|
30
|
+
*/
|
|
31
|
+
export function validateManifest(data) {
|
|
32
|
+
if (data === null || typeof data !== 'object') {
|
|
33
|
+
throw new ScheduleValidationError('Schedule manifest must be a JSON object');
|
|
34
|
+
}
|
|
35
|
+
const obj = data;
|
|
36
|
+
if (typeof obj.version !== 'number' || obj.version < 1) {
|
|
37
|
+
throw new ScheduleValidationError('Schedule manifest requires a positive integer "version" field');
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(obj.schedules)) {
|
|
40
|
+
throw new ScheduleValidationError('Schedule manifest requires a "schedules" array');
|
|
41
|
+
}
|
|
42
|
+
const seenIds = new Set();
|
|
43
|
+
for (let i = 0; i < obj.schedules.length; i++) {
|
|
44
|
+
const entry = obj.schedules[i];
|
|
45
|
+
validateEntry(entry, i, seenIds);
|
|
46
|
+
}
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
49
|
+
function validateEntry(entry, index, seenIds) {
|
|
50
|
+
if (entry === null || typeof entry !== 'object') {
|
|
51
|
+
throw new ScheduleValidationError(`schedules[${index}] must be an object`);
|
|
52
|
+
}
|
|
53
|
+
const e = entry;
|
|
54
|
+
const prefix = `schedules[${index}]`;
|
|
55
|
+
if (typeof e.id !== 'string' || e.id.length === 0) {
|
|
56
|
+
throw new ScheduleValidationError(`${prefix}.id must be a non-empty string`);
|
|
57
|
+
}
|
|
58
|
+
if (seenIds.has(e.id)) {
|
|
59
|
+
throw new ScheduleValidationError(`Duplicate schedule id: "${e.id}"`);
|
|
60
|
+
}
|
|
61
|
+
seenIds.add(e.id);
|
|
62
|
+
if (typeof e.name !== 'string' || e.name.length === 0) {
|
|
63
|
+
throw new ScheduleValidationError(`${prefix}.name must be a non-empty string`);
|
|
64
|
+
}
|
|
65
|
+
if (typeof e.enabled !== 'boolean') {
|
|
66
|
+
throw new ScheduleValidationError(`${prefix}.enabled must be a boolean`);
|
|
67
|
+
}
|
|
68
|
+
// Trigger validation
|
|
69
|
+
if (e.trigger === null || typeof e.trigger !== 'object') {
|
|
70
|
+
throw new ScheduleValidationError(`${prefix}.trigger must be an object`);
|
|
71
|
+
}
|
|
72
|
+
const trigger = e.trigger;
|
|
73
|
+
if (!VALID_TRIGGER_TYPES.includes(trigger.type)) {
|
|
74
|
+
throw new ScheduleValidationError(`${prefix}.trigger.type must be one of: ${VALID_TRIGGER_TYPES.join(', ')}`);
|
|
75
|
+
}
|
|
76
|
+
if (trigger.type === 'cron' && (typeof trigger.cron !== 'string' || trigger.cron.length === 0)) {
|
|
77
|
+
throw new ScheduleValidationError(`${prefix}.trigger.cron must be a non-empty string`);
|
|
78
|
+
}
|
|
79
|
+
if (trigger.type === 'interval') {
|
|
80
|
+
if (typeof trigger.intervalSeconds !== 'number' || trigger.intervalSeconds <= 0) {
|
|
81
|
+
throw new ScheduleValidationError(`${prefix}.trigger.intervalSeconds must be a positive number`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (trigger.type === 'event' && (typeof trigger.event !== 'string' || trigger.event.length === 0)) {
|
|
85
|
+
throw new ScheduleValidationError(`${prefix}.trigger.event must be a non-empty string`);
|
|
86
|
+
}
|
|
87
|
+
// Task validation
|
|
88
|
+
if (e.task === null || typeof e.task !== 'object') {
|
|
89
|
+
throw new ScheduleValidationError(`${prefix}.task must be an object`);
|
|
90
|
+
}
|
|
91
|
+
const task = e.task;
|
|
92
|
+
if (!VALID_TASK_TYPES.includes(task.type)) {
|
|
93
|
+
throw new ScheduleValidationError(`${prefix}.task.type must be one of: ${VALID_TASK_TYPES.join(', ')}`);
|
|
94
|
+
}
|
|
95
|
+
if (typeof task.ref !== 'string' || task.ref.length === 0) {
|
|
96
|
+
throw new ScheduleValidationError(`${prefix}.task.ref must be a non-empty string`);
|
|
97
|
+
}
|
|
98
|
+
// Providers validation
|
|
99
|
+
if (!Array.isArray(e.providers) || e.providers.length === 0) {
|
|
100
|
+
throw new ScheduleValidationError(`${prefix}.providers must be a non-empty array of strings`);
|
|
101
|
+
}
|
|
102
|
+
for (const p of e.providers) {
|
|
103
|
+
if (typeof p !== 'string' || p.length === 0) {
|
|
104
|
+
throw new ScheduleValidationError(`${prefix}.providers must contain only non-empty strings`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Optional retry validation
|
|
108
|
+
if (e.retry !== undefined) {
|
|
109
|
+
if (e.retry === null || typeof e.retry !== 'object') {
|
|
110
|
+
throw new ScheduleValidationError(`${prefix}.retry must be an object`);
|
|
111
|
+
}
|
|
112
|
+
const retry = e.retry;
|
|
113
|
+
if (typeof retry.maxRetries !== 'number' || retry.maxRetries < 0) {
|
|
114
|
+
throw new ScheduleValidationError(`${prefix}.retry.maxRetries must be a non-negative number`);
|
|
115
|
+
}
|
|
116
|
+
if (typeof retry.backoffSeconds !== 'number' || retry.backoffSeconds <= 0) {
|
|
117
|
+
throw new ScheduleValidationError(`${prefix}.retry.backoffSeconds must be a positive number`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// Schedule Operations
|
|
123
|
+
// ============================================================================
|
|
124
|
+
/**
|
|
125
|
+
* Parse and validate a schedule.json file from disk.
|
|
126
|
+
*/
|
|
127
|
+
export async function parseSchedule(filePath) {
|
|
128
|
+
let raw;
|
|
129
|
+
try {
|
|
130
|
+
raw = await readFile(filePath, 'utf8');
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
throw new ScheduleValidationError(`Cannot read schedule file: ${filePath} — ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
let data;
|
|
136
|
+
try {
|
|
137
|
+
data = JSON.parse(raw);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
throw new ScheduleValidationError(`Invalid JSON in schedule file: ${filePath}`);
|
|
141
|
+
}
|
|
142
|
+
return validateManifest(data);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Evaluate which schedules are due now, based on trigger config and state.
|
|
146
|
+
* Returns a list of entries that should be executed.
|
|
147
|
+
*/
|
|
148
|
+
export function evaluateSchedule(manifest, state, now = new Date()) {
|
|
149
|
+
const due = [];
|
|
150
|
+
for (const entry of manifest.schedules) {
|
|
151
|
+
if (!entry.enabled)
|
|
152
|
+
continue;
|
|
153
|
+
const run = state.runs[entry.id];
|
|
154
|
+
if (run?.status === 'running')
|
|
155
|
+
continue;
|
|
156
|
+
if (isDue(entry.trigger, run, now)) {
|
|
157
|
+
due.push(entry);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return due;
|
|
161
|
+
}
|
|
162
|
+
function isDue(trigger, run, now) {
|
|
163
|
+
switch (trigger.type) {
|
|
164
|
+
case 'startup':
|
|
165
|
+
// Due if never run
|
|
166
|
+
return run === undefined;
|
|
167
|
+
case 'interval': {
|
|
168
|
+
if (!run)
|
|
169
|
+
return true;
|
|
170
|
+
const lastRun = new Date(run.lastRun);
|
|
171
|
+
const elapsed = (now.getTime() - lastRun.getTime()) / 1000;
|
|
172
|
+
return elapsed >= trigger.intervalSeconds;
|
|
173
|
+
}
|
|
174
|
+
case 'cron':
|
|
175
|
+
return isCronDue(trigger.cron, run, now);
|
|
176
|
+
case 'event':
|
|
177
|
+
// Event triggers are fired externally, not by polling
|
|
178
|
+
return false;
|
|
179
|
+
default:
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Minimal cron evaluation for 5-field cron expressions.
|
|
185
|
+
* Supports: minute hour day-of-month month day-of-week
|
|
186
|
+
* Wildcard (*) and specific values only (no ranges/lists for simplicity).
|
|
187
|
+
*/
|
|
188
|
+
export function isCronDue(cron, run, now) {
|
|
189
|
+
const parts = cron.trim().split(/\s+/);
|
|
190
|
+
if (parts.length !== 5)
|
|
191
|
+
return false;
|
|
192
|
+
const [minField, hourField, domField, monthField, dowField] = parts;
|
|
193
|
+
const minute = now.getMinutes();
|
|
194
|
+
const hour = now.getHours();
|
|
195
|
+
const dom = now.getDate();
|
|
196
|
+
const month = now.getMonth() + 1; // 1-12
|
|
197
|
+
const dow = now.getDay(); // 0-6 (Sun-Sat)
|
|
198
|
+
if (!cronFieldMatches(minField, minute))
|
|
199
|
+
return false;
|
|
200
|
+
if (!cronFieldMatches(hourField, hour))
|
|
201
|
+
return false;
|
|
202
|
+
if (!cronFieldMatches(domField, dom))
|
|
203
|
+
return false;
|
|
204
|
+
if (!cronFieldMatches(monthField, month))
|
|
205
|
+
return false;
|
|
206
|
+
if (!cronFieldMatches(dowField, dow))
|
|
207
|
+
return false;
|
|
208
|
+
// Don't run again within the same minute
|
|
209
|
+
if (run) {
|
|
210
|
+
const lastRun = new Date(run.lastRun);
|
|
211
|
+
if (lastRun.getFullYear() === now.getFullYear() &&
|
|
212
|
+
lastRun.getMonth() === now.getMonth() &&
|
|
213
|
+
lastRun.getDate() === now.getDate() &&
|
|
214
|
+
lastRun.getHours() === now.getHours() &&
|
|
215
|
+
lastRun.getMinutes() === now.getMinutes()) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
function cronFieldMatches(field, value) {
|
|
222
|
+
if (field === '*')
|
|
223
|
+
return true;
|
|
224
|
+
// Support */N step syntax
|
|
225
|
+
if (field.startsWith('*/')) {
|
|
226
|
+
const step = parseInt(field.slice(2), 10);
|
|
227
|
+
if (isNaN(step) || step <= 0)
|
|
228
|
+
return false;
|
|
229
|
+
return value % step === 0;
|
|
230
|
+
}
|
|
231
|
+
// Support comma-separated values
|
|
232
|
+
const values = field.split(',').map(v => parseInt(v, 10));
|
|
233
|
+
return values.includes(value);
|
|
234
|
+
}
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// Task Execution
|
|
237
|
+
// ============================================================================
|
|
238
|
+
/**
|
|
239
|
+
* Execute a scheduled task using the specified provider.
|
|
240
|
+
* Includes retry logic if configured.
|
|
241
|
+
*/
|
|
242
|
+
export async function executeTask(entry, provider) {
|
|
243
|
+
const maxRetries = entry.retry?.maxRetries ?? 0;
|
|
244
|
+
const backoffSeconds = entry.retry?.backoffSeconds ?? 1;
|
|
245
|
+
let lastResult = { success: false, error: 'No attempt made' };
|
|
246
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
247
|
+
if (attempt > 0) {
|
|
248
|
+
const delay = backoffSeconds * Math.pow(2, attempt - 1) * 1000;
|
|
249
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
lastResult = await provider.execute(entry);
|
|
253
|
+
if (lastResult.success)
|
|
254
|
+
return lastResult;
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
lastResult = { success: false, error: err.message };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return lastResult;
|
|
261
|
+
}
|
|
262
|
+
// ============================================================================
|
|
263
|
+
// State Persistence
|
|
264
|
+
// ============================================================================
|
|
265
|
+
/**
|
|
266
|
+
* Load schedule state from disk. Returns empty state if file doesn't exist.
|
|
267
|
+
*/
|
|
268
|
+
export async function loadState(statePath) {
|
|
269
|
+
try {
|
|
270
|
+
const raw = await readFile(statePath, 'utf8');
|
|
271
|
+
return JSON.parse(raw);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return { runs: {} };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Save schedule state to disk.
|
|
279
|
+
*/
|
|
280
|
+
export async function saveState(statePath, state) {
|
|
281
|
+
await writeFile(statePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
282
|
+
}
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// Built-in Providers
|
|
285
|
+
// ============================================================================
|
|
286
|
+
/**
|
|
287
|
+
* LocalPollingProvider — evaluates schedule in the ralph-watch loop.
|
|
288
|
+
* Executes tasks as local processes or stubs.
|
|
289
|
+
*/
|
|
290
|
+
export class LocalPollingProvider {
|
|
291
|
+
name = 'local-polling';
|
|
292
|
+
async execute(entry) {
|
|
293
|
+
switch (entry.task.type) {
|
|
294
|
+
case 'script': {
|
|
295
|
+
try {
|
|
296
|
+
const { execSync } = await import('node:child_process');
|
|
297
|
+
const output = execSync(entry.task.ref, {
|
|
298
|
+
encoding: 'utf8',
|
|
299
|
+
timeout: 60_000,
|
|
300
|
+
});
|
|
301
|
+
return { success: true, output: output.trim() };
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
return { success: false, error: err.message };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
case 'workflow':
|
|
308
|
+
return {
|
|
309
|
+
success: true,
|
|
310
|
+
output: `Workflow ${entry.task.ref} triggered (local stub — use github-actions provider for real dispatch)`,
|
|
311
|
+
};
|
|
312
|
+
case 'copilot':
|
|
313
|
+
return {
|
|
314
|
+
success: true,
|
|
315
|
+
output: `Copilot task ${entry.task.ref} dispatched (stub)`,
|
|
316
|
+
};
|
|
317
|
+
case 'webhook': {
|
|
318
|
+
try {
|
|
319
|
+
const resp = await fetch(entry.task.ref, {
|
|
320
|
+
method: 'POST',
|
|
321
|
+
headers: { 'Content-Type': 'application/json' },
|
|
322
|
+
body: JSON.stringify({ scheduleId: entry.id, timestamp: new Date().toISOString() }),
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
success: resp.ok,
|
|
326
|
+
output: `HTTP ${resp.status} ${resp.statusText}`,
|
|
327
|
+
error: resp.ok ? undefined : `HTTP ${resp.status}`,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
return { success: false, error: err.message };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
default:
|
|
335
|
+
return { success: false, error: `Unknown task type: ${entry.task.type}` };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* GitHubActionsProvider — generates workflow YAML files from schedule manifest.
|
|
341
|
+
*/
|
|
342
|
+
export class GitHubActionsProvider {
|
|
343
|
+
name = 'github-actions';
|
|
344
|
+
async execute(entry) {
|
|
345
|
+
// GitHub Actions execution is handled by the platform itself.
|
|
346
|
+
// This provider's main value is generate(), not execute().
|
|
347
|
+
return {
|
|
348
|
+
success: true,
|
|
349
|
+
output: `GitHub Actions handles execution of ${entry.task.ref} natively`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
async generate(manifest, outDir) {
|
|
353
|
+
const generated = [];
|
|
354
|
+
for (const entry of manifest.schedules) {
|
|
355
|
+
if (!entry.enabled)
|
|
356
|
+
continue;
|
|
357
|
+
if (!entry.providers.includes('github-actions'))
|
|
358
|
+
continue;
|
|
359
|
+
if (entry.trigger.type !== 'cron' && entry.trigger.type !== 'interval')
|
|
360
|
+
continue;
|
|
361
|
+
const cronExpr = entry.trigger.type === 'cron'
|
|
362
|
+
? entry.trigger.cron
|
|
363
|
+
: intervalToCron(entry.trigger.intervalSeconds);
|
|
364
|
+
const workflowName = `squad-schedule-${entry.id}.yml`;
|
|
365
|
+
const workflowPath = path.join(outDir, workflowName);
|
|
366
|
+
const yaml = [
|
|
367
|
+
`# Auto-generated by Squad Scheduler from schedule.json`,
|
|
368
|
+
`# Schedule: ${entry.name} (${entry.id})`,
|
|
369
|
+
`name: "Squad: ${entry.name}"`,
|
|
370
|
+
``,
|
|
371
|
+
`on:`,
|
|
372
|
+
` schedule:`,
|
|
373
|
+
` - cron: '${cronExpr}'`,
|
|
374
|
+
` workflow_dispatch: {}`,
|
|
375
|
+
``,
|
|
376
|
+
`jobs:`,
|
|
377
|
+
` run:`,
|
|
378
|
+
` runs-on: ubuntu-latest`,
|
|
379
|
+
` steps:`,
|
|
380
|
+
` - uses: actions/checkout@v4`,
|
|
381
|
+
` - name: Run scheduled task`,
|
|
382
|
+
` run: echo "Executing ${entry.id} — ${entry.task.type}:${entry.task.ref}"`,
|
|
383
|
+
].join('\n') + '\n';
|
|
384
|
+
const dir = path.dirname(workflowPath);
|
|
385
|
+
if (!fs.existsSync(dir)) {
|
|
386
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
387
|
+
}
|
|
388
|
+
fs.writeFileSync(workflowPath, yaml, 'utf8');
|
|
389
|
+
generated.push(workflowPath);
|
|
390
|
+
}
|
|
391
|
+
return generated;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Convert an interval in seconds to the nearest cron expression.
|
|
396
|
+
*/
|
|
397
|
+
function intervalToCron(seconds) {
|
|
398
|
+
const minutes = Math.max(1, Math.round(seconds / 60));
|
|
399
|
+
if (minutes < 60)
|
|
400
|
+
return `*/${minutes} * * * *`;
|
|
401
|
+
const hours = Math.round(minutes / 60);
|
|
402
|
+
if (hours < 24)
|
|
403
|
+
return `0 */${hours} * * *`;
|
|
404
|
+
return '0 0 * * *'; // daily fallback
|
|
405
|
+
}
|
|
406
|
+
// ============================================================================
|
|
407
|
+
// Default Template
|
|
408
|
+
// ============================================================================
|
|
409
|
+
/**
|
|
410
|
+
* Default schedule.json template for `squad schedule init`.
|
|
411
|
+
*/
|
|
412
|
+
export function defaultScheduleTemplate() {
|
|
413
|
+
return {
|
|
414
|
+
version: 1,
|
|
415
|
+
schedules: [
|
|
416
|
+
{
|
|
417
|
+
id: 'ralph-heartbeat',
|
|
418
|
+
name: 'Ralph Heartbeat',
|
|
419
|
+
enabled: true,
|
|
420
|
+
trigger: { type: 'interval', intervalSeconds: 300 },
|
|
421
|
+
task: { type: 'workflow', ref: '.github/workflows/squad-heartbeat.yml' },
|
|
422
|
+
providers: ['local-polling', 'github-actions'],
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
//# sourceMappingURL=scheduler.js.map
|