@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.
Files changed (202) hide show
  1. package/dist/adapter/client.d.ts +17 -0
  2. package/dist/adapter/client.d.ts.map +1 -1
  3. package/dist/adapter/client.js +101 -1
  4. package/dist/adapter/client.js.map +1 -1
  5. package/dist/agents/history-shadow.d.ts.map +1 -1
  6. package/dist/agents/history-shadow.js +99 -32
  7. package/dist/agents/history-shadow.js.map +1 -1
  8. package/dist/agents/index.d.ts +1 -0
  9. package/dist/agents/index.d.ts.map +1 -1
  10. package/dist/agents/index.js +2 -0
  11. package/dist/agents/index.js.map +1 -1
  12. package/dist/agents/model-selector.d.ts +2 -0
  13. package/dist/agents/model-selector.d.ts.map +1 -1
  14. package/dist/agents/model-selector.js +41 -35
  15. package/dist/agents/model-selector.js.map +1 -1
  16. package/dist/agents/personal.d.ts +35 -0
  17. package/dist/agents/personal.d.ts.map +1 -0
  18. package/dist/agents/personal.js +67 -0
  19. package/dist/agents/personal.js.map +1 -0
  20. package/dist/builders/index.d.ts +3 -2
  21. package/dist/builders/index.d.ts.map +1 -1
  22. package/dist/builders/index.js +28 -0
  23. package/dist/builders/index.js.map +1 -1
  24. package/dist/builders/types.d.ts +13 -0
  25. package/dist/builders/types.d.ts.map +1 -1
  26. package/dist/config/init.d.ts +8 -0
  27. package/dist/config/init.d.ts.map +1 -1
  28. package/dist/config/init.js +131 -20
  29. package/dist/config/init.js.map +1 -1
  30. package/dist/config/models.d.ts +112 -0
  31. package/dist/config/models.d.ts.map +1 -1
  32. package/dist/config/models.js +329 -18
  33. package/dist/config/models.js.map +1 -1
  34. package/dist/coordinator/index.js +2 -2
  35. package/dist/coordinator/index.js.map +1 -1
  36. package/dist/index.d.ts +8 -3
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +7 -2
  39. package/dist/index.js.map +1 -1
  40. package/dist/platform/azure-devops.d.ts +42 -0
  41. package/dist/platform/azure-devops.d.ts.map +1 -1
  42. package/dist/platform/azure-devops.js +75 -0
  43. package/dist/platform/azure-devops.js.map +1 -1
  44. package/dist/platform/comms-file-log.d.ts.map +1 -1
  45. package/dist/platform/comms-file-log.js +2 -1
  46. package/dist/platform/comms-file-log.js.map +1 -1
  47. package/dist/platform/index.d.ts +2 -1
  48. package/dist/platform/index.d.ts.map +1 -1
  49. package/dist/platform/index.js +1 -0
  50. package/dist/platform/index.js.map +1 -1
  51. package/dist/ralph/capabilities.d.ts +67 -0
  52. package/dist/ralph/capabilities.d.ts.map +1 -0
  53. package/dist/ralph/capabilities.js +111 -0
  54. package/dist/ralph/capabilities.js.map +1 -0
  55. package/dist/ralph/index.d.ts +2 -0
  56. package/dist/ralph/index.d.ts.map +1 -1
  57. package/dist/ralph/index.js +6 -5
  58. package/dist/ralph/index.js.map +1 -1
  59. package/dist/ralph/rate-limiting.d.ts +99 -0
  60. package/dist/ralph/rate-limiting.d.ts.map +1 -0
  61. package/dist/ralph/rate-limiting.js +170 -0
  62. package/dist/ralph/rate-limiting.js.map +1 -0
  63. package/dist/resolution.d.ts +24 -2
  64. package/dist/resolution.d.ts.map +1 -1
  65. package/dist/resolution.js +106 -6
  66. package/dist/resolution.js.map +1 -1
  67. package/dist/roles/catalog-categories.d.ts +146 -0
  68. package/dist/roles/catalog-categories.d.ts.map +1 -0
  69. package/dist/roles/catalog-categories.js +374 -0
  70. package/dist/roles/catalog-categories.js.map +1 -0
  71. package/dist/roles/catalog-engineering.d.ts +212 -0
  72. package/dist/roles/catalog-engineering.d.ts.map +1 -0
  73. package/dist/roles/catalog-engineering.js +549 -0
  74. package/dist/roles/catalog-engineering.js.map +1 -0
  75. package/dist/roles/catalog.d.ts +24 -0
  76. package/dist/roles/catalog.d.ts.map +1 -0
  77. package/dist/roles/catalog.js +28 -0
  78. package/dist/roles/catalog.js.map +1 -0
  79. package/dist/roles/index.d.ts +69 -0
  80. package/dist/roles/index.d.ts.map +1 -0
  81. package/dist/roles/index.js +197 -0
  82. package/dist/roles/index.js.map +1 -0
  83. package/dist/roles/types.d.ts +87 -0
  84. package/dist/roles/types.d.ts.map +1 -0
  85. package/dist/roles/types.js +14 -0
  86. package/dist/roles/types.js.map +1 -0
  87. package/dist/runtime/benchmarks.js +5 -5
  88. package/dist/runtime/benchmarks.js.map +1 -1
  89. package/dist/runtime/constants.d.ts +2 -2
  90. package/dist/runtime/constants.d.ts.map +1 -1
  91. package/dist/runtime/constants.js +5 -3
  92. package/dist/runtime/constants.js.map +1 -1
  93. package/dist/runtime/cross-squad.d.ts +118 -0
  94. package/dist/runtime/cross-squad.d.ts.map +1 -0
  95. package/dist/runtime/cross-squad.js +234 -0
  96. package/dist/runtime/cross-squad.js.map +1 -0
  97. package/dist/runtime/otel-init.d.ts +24 -17
  98. package/dist/runtime/otel-init.d.ts.map +1 -1
  99. package/dist/runtime/otel-init.js +29 -20
  100. package/dist/runtime/otel-init.js.map +1 -1
  101. package/dist/runtime/otel-metrics.d.ts +5 -0
  102. package/dist/runtime/otel-metrics.d.ts.map +1 -1
  103. package/dist/runtime/otel-metrics.js +54 -0
  104. package/dist/runtime/otel-metrics.js.map +1 -1
  105. package/dist/runtime/rework.d.ts +71 -0
  106. package/dist/runtime/rework.d.ts.map +1 -0
  107. package/dist/runtime/rework.js +107 -0
  108. package/dist/runtime/rework.js.map +1 -0
  109. package/dist/runtime/scheduler.d.ts +128 -0
  110. package/dist/runtime/scheduler.d.ts.map +1 -0
  111. package/dist/runtime/scheduler.js +427 -0
  112. package/dist/runtime/scheduler.js.map +1 -0
  113. package/dist/runtime/squad-observer.d.ts.map +1 -1
  114. package/dist/runtime/squad-observer.js +4 -0
  115. package/dist/runtime/squad-observer.js.map +1 -1
  116. package/dist/runtime/streaming.d.ts +2 -0
  117. package/dist/runtime/streaming.d.ts.map +1 -1
  118. package/dist/runtime/streaming.js +6 -0
  119. package/dist/runtime/streaming.js.map +1 -1
  120. package/dist/runtime/telemetry.d.ts +2 -0
  121. package/dist/runtime/telemetry.d.ts.map +1 -1
  122. package/dist/runtime/telemetry.js +6 -0
  123. package/dist/runtime/telemetry.js.map +1 -1
  124. package/dist/sharing/consult.d.ts +2 -2
  125. package/dist/sharing/consult.js +6 -6
  126. package/dist/sharing/consult.js.map +1 -1
  127. package/dist/sharing/export.d.ts.map +1 -1
  128. package/dist/sharing/export.js +17 -4
  129. package/dist/sharing/export.js.map +1 -1
  130. package/dist/skills/handler-types.d.ts +271 -0
  131. package/dist/skills/handler-types.d.ts.map +1 -0
  132. package/dist/skills/handler-types.js +31 -0
  133. package/dist/skills/handler-types.js.map +1 -0
  134. package/dist/skills/index.d.ts +3 -0
  135. package/dist/skills/index.d.ts.map +1 -1
  136. package/dist/skills/index.js +3 -0
  137. package/dist/skills/index.js.map +1 -1
  138. package/dist/skills/skill-script-loader.d.ts +65 -0
  139. package/dist/skills/skill-script-loader.d.ts.map +1 -0
  140. package/dist/skills/skill-script-loader.js +227 -0
  141. package/dist/skills/skill-script-loader.js.map +1 -0
  142. package/dist/skills/skill-source.d.ts.map +1 -1
  143. package/dist/skills/skill-source.js +5 -1
  144. package/dist/skills/skill-source.js.map +1 -1
  145. package/dist/tools/index.d.ts +10 -1
  146. package/dist/tools/index.d.ts.map +1 -1
  147. package/dist/tools/index.js +49 -8
  148. package/dist/tools/index.js.map +1 -1
  149. package/dist/upstream/resolver.d.ts.map +1 -1
  150. package/dist/upstream/resolver.js +14 -5
  151. package/dist/upstream/resolver.js.map +1 -1
  152. package/package.json +34 -3
  153. package/templates/casting/Futurama.json +10 -0
  154. package/templates/casting-policy.json +4 -2
  155. package/templates/casting-reference.md +104 -0
  156. package/templates/cooperative-rate-limiting.md +229 -0
  157. package/templates/issue-lifecycle.md +412 -0
  158. package/templates/keda-scaler.md +164 -0
  159. package/templates/machine-capabilities.md +75 -0
  160. package/templates/mcp-config.md +0 -8
  161. package/templates/orchestration-log.md +27 -27
  162. package/templates/package.json +3 -0
  163. package/templates/ralph-circuit-breaker.md +313 -0
  164. package/templates/ralph-triage.js +543 -0
  165. package/templates/routing.md +5 -20
  166. package/templates/schedule.json +19 -0
  167. package/templates/scribe-charter.md +1 -1
  168. package/templates/skills/agent-collaboration/SKILL.md +42 -0
  169. package/templates/skills/agent-conduct/SKILL.md +24 -0
  170. package/templates/skills/architectural-proposals/SKILL.md +151 -0
  171. package/templates/skills/ci-validation-gates/SKILL.md +84 -0
  172. package/templates/skills/cli-wiring/SKILL.md +47 -0
  173. package/templates/skills/client-compatibility/SKILL.md +89 -0
  174. package/templates/skills/cross-squad/SKILL.md +114 -0
  175. package/templates/skills/distributed-mesh/SKILL.md +287 -0
  176. package/templates/skills/distributed-mesh/mesh.json.example +30 -0
  177. package/templates/skills/distributed-mesh/sync-mesh.ps1 +111 -0
  178. package/templates/skills/distributed-mesh/sync-mesh.sh +104 -0
  179. package/templates/skills/docs-standards/SKILL.md +71 -0
  180. package/templates/skills/economy-mode/SKILL.md +114 -0
  181. package/templates/skills/external-comms/SKILL.md +329 -0
  182. package/templates/skills/gh-auth-isolation/SKILL.md +183 -0
  183. package/templates/skills/git-workflow/SKILL.md +204 -0
  184. package/templates/skills/github-multi-account/SKILL.md +95 -0
  185. package/templates/skills/history-hygiene/SKILL.md +36 -0
  186. package/templates/skills/humanizer/SKILL.md +105 -0
  187. package/templates/skills/init-mode/SKILL.md +102 -0
  188. package/templates/skills/model-selection/SKILL.md +117 -0
  189. package/templates/skills/nap/SKILL.md +24 -0
  190. package/templates/skills/personal-squad/SKILL.md +57 -0
  191. package/templates/skills/release-process/SKILL.md +423 -0
  192. package/templates/skills/reskill/SKILL.md +92 -0
  193. package/templates/skills/reviewer-protocol/SKILL.md +79 -0
  194. package/templates/skills/secret-handling/SKILL.md +200 -0
  195. package/templates/skills/session-recovery/SKILL.md +155 -0
  196. package/templates/skills/squad-conventions/SKILL.md +69 -0
  197. package/templates/skills/test-discipline/SKILL.md +37 -0
  198. package/templates/skills/windows-compatibility/SKILL.md +74 -0
  199. package/templates/squad.agent.md +1287 -1146
  200. package/templates/workflows/squad-docs.yml +8 -4
  201. package/templates/workflows/squad-heartbeat.yml +55 -200
  202. 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