@studio-foundation/engine 0.3.0-beta.1 → 0.3.0-beta.5

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 (246) hide show
  1. package/dist/__tests__/engine.conditions-group.test.d.ts +2 -0
  2. package/dist/__tests__/engine.conditions-group.test.d.ts.map +1 -0
  3. package/dist/__tests__/engine.conditions-group.test.js +222 -0
  4. package/dist/__tests__/engine.conditions-group.test.js.map +1 -0
  5. package/dist/__tests__/engine.conditions.test.d.ts +2 -0
  6. package/dist/__tests__/engine.conditions.test.d.ts.map +1 -0
  7. package/dist/__tests__/engine.conditions.test.js +164 -0
  8. package/dist/__tests__/engine.conditions.test.js.map +1 -0
  9. package/dist/__tests__/engine.resume.test.d.ts +2 -0
  10. package/dist/__tests__/engine.resume.test.d.ts.map +1 -0
  11. package/dist/__tests__/engine.resume.test.js +93 -0
  12. package/dist/__tests__/engine.resume.test.js.map +1 -0
  13. package/dist/__tests__/engine.script-stage.test.d.ts +2 -0
  14. package/dist/__tests__/engine.script-stage.test.d.ts.map +1 -0
  15. package/dist/__tests__/engine.script-stage.test.js +108 -0
  16. package/dist/__tests__/engine.script-stage.test.js.map +1 -0
  17. package/dist/db/client.d.ts +3 -0
  18. package/dist/db/client.d.ts.map +1 -0
  19. package/{src/db/client.ts → dist/db/client.js} +1 -2
  20. package/dist/db/client.js.map +1 -0
  21. package/dist/engine.context-event.test.d.ts +2 -0
  22. package/dist/engine.context-event.test.d.ts.map +1 -0
  23. package/dist/engine.context-event.test.js +111 -0
  24. package/dist/engine.context-event.test.js.map +1 -0
  25. package/dist/engine.d.ts +52 -0
  26. package/dist/engine.d.ts.map +1 -0
  27. package/dist/engine.js +364 -0
  28. package/dist/engine.js.map +1 -0
  29. package/dist/events.d.ts +163 -0
  30. package/dist/events.d.ts.map +1 -0
  31. package/dist/events.js +12 -0
  32. package/dist/events.js.map +1 -0
  33. package/{src/index.ts → dist/index.d.ts} +3 -45
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +24 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/pipeline/agent-loader.d.ts +4 -0
  38. package/dist/pipeline/agent-loader.d.ts.map +1 -0
  39. package/dist/pipeline/agent-loader.js +28 -0
  40. package/dist/pipeline/agent-loader.js.map +1 -0
  41. package/dist/pipeline/agent-loader.test.d.ts +2 -0
  42. package/dist/pipeline/agent-loader.test.d.ts.map +1 -0
  43. package/dist/pipeline/agent-loader.test.js +134 -0
  44. package/dist/pipeline/agent-loader.test.js.map +1 -0
  45. package/dist/pipeline/condition-evaluator.d.ts +6 -0
  46. package/dist/pipeline/condition-evaluator.d.ts.map +1 -0
  47. package/dist/pipeline/condition-evaluator.js +105 -0
  48. package/dist/pipeline/condition-evaluator.js.map +1 -0
  49. package/dist/pipeline/condition-evaluator.test.d.ts +2 -0
  50. package/dist/pipeline/condition-evaluator.test.d.ts.map +1 -0
  51. package/dist/pipeline/condition-evaluator.test.js +103 -0
  52. package/dist/pipeline/condition-evaluator.test.js.map +1 -0
  53. package/dist/pipeline/context-pack-loader.d.ts +3 -0
  54. package/dist/pipeline/context-pack-loader.d.ts.map +1 -0
  55. package/dist/pipeline/context-pack-loader.js +51 -0
  56. package/dist/pipeline/context-pack-loader.js.map +1 -0
  57. package/dist/pipeline/context-propagation.d.ts +28 -0
  58. package/dist/pipeline/context-propagation.d.ts.map +1 -0
  59. package/dist/pipeline/context-propagation.js +161 -0
  60. package/dist/pipeline/context-propagation.js.map +1 -0
  61. package/dist/pipeline/context-propagation.test.d.ts +2 -0
  62. package/dist/pipeline/context-propagation.test.d.ts.map +1 -0
  63. package/dist/pipeline/context-propagation.test.js +194 -0
  64. package/dist/pipeline/context-propagation.test.js.map +1 -0
  65. package/dist/pipeline/contract-loader.d.ts +4 -0
  66. package/dist/pipeline/contract-loader.d.ts.map +1 -0
  67. package/dist/pipeline/contract-loader.js +30 -0
  68. package/dist/pipeline/contract-loader.js.map +1 -0
  69. package/dist/pipeline/group-orchestrator.d.ts +19 -0
  70. package/dist/pipeline/group-orchestrator.d.ts.map +1 -0
  71. package/dist/pipeline/group-orchestrator.js +376 -0
  72. package/dist/pipeline/group-orchestrator.js.map +1 -0
  73. package/dist/pipeline/hook-executor.d.ts +28 -0
  74. package/dist/pipeline/hook-executor.d.ts.map +1 -0
  75. package/dist/pipeline/hook-executor.js +63 -0
  76. package/dist/pipeline/hook-executor.js.map +1 -0
  77. package/dist/pipeline/hook-executor.test.d.ts +2 -0
  78. package/dist/pipeline/hook-executor.test.d.ts.map +1 -0
  79. package/dist/pipeline/hook-executor.test.js +71 -0
  80. package/dist/pipeline/hook-executor.test.js.map +1 -0
  81. package/dist/pipeline/invariants-loader.d.ts +6 -0
  82. package/dist/pipeline/invariants-loader.d.ts.map +1 -0
  83. package/dist/pipeline/invariants-loader.js +15 -0
  84. package/dist/pipeline/invariants-loader.js.map +1 -0
  85. package/dist/pipeline/invariants-loader.test.d.ts +2 -0
  86. package/dist/pipeline/invariants-loader.test.d.ts.map +1 -0
  87. package/dist/pipeline/invariants-loader.test.js +39 -0
  88. package/dist/pipeline/invariants-loader.test.js.map +1 -0
  89. package/dist/pipeline/loader.d.ts +5 -0
  90. package/dist/pipeline/loader.d.ts.map +1 -0
  91. package/dist/pipeline/loader.js +132 -0
  92. package/dist/pipeline/loader.js.map +1 -0
  93. package/dist/pipeline/loader.test.d.ts +2 -0
  94. package/dist/pipeline/loader.test.d.ts.map +1 -0
  95. package/dist/pipeline/loader.test.js +120 -0
  96. package/dist/pipeline/loader.test.js.map +1 -0
  97. package/dist/pipeline/output-validator.d.ts +10 -0
  98. package/dist/pipeline/output-validator.d.ts.map +1 -0
  99. package/dist/pipeline/output-validator.js +20 -0
  100. package/dist/pipeline/output-validator.js.map +1 -0
  101. package/dist/pipeline/output-validator.test.d.ts +2 -0
  102. package/dist/pipeline/output-validator.test.d.ts.map +1 -0
  103. package/dist/pipeline/output-validator.test.js +77 -0
  104. package/dist/pipeline/output-validator.test.js.map +1 -0
  105. package/dist/pipeline/post-validator.d.ts +8 -0
  106. package/dist/pipeline/post-validator.d.ts.map +1 -0
  107. package/dist/pipeline/post-validator.js +103 -0
  108. package/dist/pipeline/post-validator.js.map +1 -0
  109. package/dist/pipeline/skill-loader.d.ts +10 -0
  110. package/dist/pipeline/skill-loader.d.ts.map +1 -0
  111. package/dist/pipeline/skill-loader.js +26 -0
  112. package/dist/pipeline/skill-loader.js.map +1 -0
  113. package/dist/pipeline/skill-loader.test.d.ts +2 -0
  114. package/dist/pipeline/skill-loader.test.d.ts.map +1 -0
  115. package/dist/pipeline/skill-loader.test.js +38 -0
  116. package/dist/pipeline/skill-loader.test.js.map +1 -0
  117. package/dist/pipeline/stage-executor.d.ts +26 -0
  118. package/dist/pipeline/stage-executor.d.ts.map +1 -0
  119. package/dist/pipeline/stage-executor.js +540 -0
  120. package/dist/pipeline/stage-executor.js.map +1 -0
  121. package/dist/pipeline/stage-resolver.d.ts +3 -0
  122. package/dist/pipeline/stage-resolver.d.ts.map +1 -0
  123. package/dist/pipeline/stage-resolver.js +6 -0
  124. package/dist/pipeline/stage-resolver.js.map +1 -0
  125. package/dist/pipeline/startup-executor.d.ts +3 -0
  126. package/dist/pipeline/startup-executor.d.ts.map +1 -0
  127. package/dist/pipeline/startup-executor.js +23 -0
  128. package/dist/pipeline/startup-executor.js.map +1 -0
  129. package/dist/pipeline/startup-executor.test.d.ts +2 -0
  130. package/dist/pipeline/startup-executor.test.d.ts.map +1 -0
  131. package/dist/pipeline/startup-executor.test.js +34 -0
  132. package/dist/pipeline/startup-executor.test.js.map +1 -0
  133. package/dist/pipeline/types.d.ts +28 -0
  134. package/dist/pipeline/types.d.ts.map +1 -0
  135. package/dist/pipeline/types.js +12 -0
  136. package/dist/pipeline/types.js.map +1 -0
  137. package/dist/repo-resolver.d.ts +10 -0
  138. package/dist/repo-resolver.d.ts.map +1 -0
  139. package/dist/repo-resolver.js +39 -0
  140. package/dist/repo-resolver.js.map +1 -0
  141. package/dist/spawners/direct-engine-spawner.d.ts +8 -0
  142. package/dist/spawners/direct-engine-spawner.d.ts.map +1 -0
  143. package/dist/spawners/direct-engine-spawner.js +24 -0
  144. package/dist/spawners/direct-engine-spawner.js.map +1 -0
  145. package/dist/state/run-store.d.ts +74 -0
  146. package/dist/state/run-store.d.ts.map +1 -0
  147. package/dist/state/run-store.js +269 -0
  148. package/dist/state/run-store.js.map +1 -0
  149. package/dist/state/run-store.test.d.ts +2 -0
  150. package/dist/state/run-store.test.d.ts.map +1 -0
  151. package/dist/state/run-store.test.js +130 -0
  152. package/dist/state/run-store.test.js.map +1 -0
  153. package/dist/state/state-machine.d.ts +6 -0
  154. package/dist/state/state-machine.d.ts.map +1 -0
  155. package/dist/state/state-machine.js +27 -0
  156. package/dist/state/state-machine.js.map +1 -0
  157. package/dist/state/status-derivation.d.ts +12 -0
  158. package/dist/state/status-derivation.d.ts.map +1 -0
  159. package/dist/state/status-derivation.js +30 -0
  160. package/dist/state/status-derivation.js.map +1 -0
  161. package/package.json +7 -4
  162. package/ARCHITECTURE.md +0 -57
  163. package/src/__tests__/__fixtures__/script-stage/contracts/book-context.contract.yaml +0 -4
  164. package/src/__tests__/engine.conditions-group.test.ts +0 -244
  165. package/src/__tests__/engine.conditions.test.ts +0 -186
  166. package/src/__tests__/engine.resume.test.ts +0 -108
  167. package/src/__tests__/engine.script-stage.test.ts +0 -125
  168. package/src/engine.context-event.test.ts +0 -175
  169. package/src/engine.ts +0 -491
  170. package/src/events.ts +0 -167
  171. package/src/pipeline/agent-loader.test.ts +0 -151
  172. package/src/pipeline/agent-loader.ts +0 -39
  173. package/src/pipeline/condition-evaluator.test.ts +0 -129
  174. package/src/pipeline/condition-evaluator.ts +0 -121
  175. package/src/pipeline/context-pack-loader.ts +0 -63
  176. package/src/pipeline/context-propagation.test.ts +0 -237
  177. package/src/pipeline/context-propagation.ts +0 -235
  178. package/src/pipeline/contract-loader.ts +0 -41
  179. package/src/pipeline/group-orchestrator.ts +0 -483
  180. package/src/pipeline/hook-executor.test.ts +0 -121
  181. package/src/pipeline/hook-executor.ts +0 -87
  182. package/src/pipeline/invariants-loader.test.ts +0 -51
  183. package/src/pipeline/invariants-loader.ts +0 -14
  184. package/src/pipeline/loader.test.ts +0 -128
  185. package/src/pipeline/loader.ts +0 -149
  186. package/src/pipeline/output-validator.test.ts +0 -100
  187. package/src/pipeline/output-validator.ts +0 -40
  188. package/src/pipeline/post-validator.ts +0 -124
  189. package/src/pipeline/skill-loader.test.ts +0 -44
  190. package/src/pipeline/skill-loader.ts +0 -32
  191. package/src/pipeline/stage-executor.ts +0 -654
  192. package/src/pipeline/stage-resolver.ts +0 -8
  193. package/src/pipeline/startup-executor.test.ts +0 -37
  194. package/src/pipeline/startup-executor.ts +0 -32
  195. package/src/pipeline/types.ts +0 -42
  196. package/src/repo-resolver.ts +0 -61
  197. package/src/spawners/direct-engine-spawner.ts +0 -26
  198. package/src/state/run-store.test.ts +0 -157
  199. package/src/state/run-store.ts +0 -362
  200. package/src/state/state-machine.ts +0 -35
  201. package/src/state/status-derivation.ts +0 -36
  202. package/tests/context-pack-loader.test.ts +0 -113
  203. package/tests/context-propagation.test.ts +0 -267
  204. package/tests/direct-engine-spawner.test.ts +0 -102
  205. package/tests/e2e/feature-v5.test.ts +0 -57
  206. package/tests/events.test.ts +0 -56
  207. package/tests/fixtures/agents/test-agent.agent.yaml +0 -5
  208. package/tests/fixtures/contracts/code-gen.contract.yaml +0 -6
  209. package/tests/fixtures/contracts/qa-gate.contract.yaml +0 -15
  210. package/tests/fixtures/contracts/test-contract.contract.yaml +0 -6
  211. package/tests/fixtures/pipelines/group-test.pipeline.yaml +0 -40
  212. package/tests/fixtures/pipelines/simple.pipeline.yaml +0 -15
  213. package/tests/fixtures/pipelines/two-stage.pipeline.yaml +0 -24
  214. package/tests/fixtures/software/pipelines/feature-builder.pipeline.yaml +0 -75
  215. package/tests/fixtures/test-project/agents/test-agent.agent.yaml +0 -5
  216. package/tests/fixtures/test-project/contracts/basic-result.contract.yaml +0 -6
  217. package/tests/fixtures/test-project/contracts/code-gen.contract.yaml +0 -6
  218. package/tests/fixtures/test-project/contracts/maximum-only.contract.yaml +0 -8
  219. package/tests/fixtures/test-project/contracts/qa-gate.contract.yaml +0 -15
  220. package/tests/fixtures/test-project/contracts/strict-result.contract.yaml +0 -7
  221. package/tests/fixtures/test-project/contracts/test-contract.contract.yaml +0 -6
  222. package/tests/fixtures/test-project/pipelines/group-simple.pipeline.yaml +0 -26
  223. package/tests/fixtures/test-project/pipelines/group-test.pipeline.yaml +0 -40
  224. package/tests/fixtures/test-project/pipelines/hook-output-template.pipeline.yaml +0 -19
  225. package/tests/fixtures/test-project/pipelines/hook-reject-on-failure.pipeline.yaml +0 -19
  226. package/tests/fixtures/test-project/pipelines/maximum-only.pipeline.yaml +0 -15
  227. package/tests/fixtures/test-project/pipelines/parallel-collect-all-test.pipeline.yaml +0 -39
  228. package/tests/fixtures/test-project/pipelines/parallel-context-isolation-test.pipeline.yaml +0 -38
  229. package/tests/fixtures/test-project/pipelines/parallel-fail-test.pipeline.yaml +0 -39
  230. package/tests/fixtures/test-project/pipelines/parallel-test.pipeline.yaml +0 -39
  231. package/tests/fixtures/test-project/pipelines/parallel-then-sequential-test.pipeline.yaml +0 -38
  232. package/tests/fixtures/test-project/pipelines/simple.pipeline.yaml +0 -15
  233. package/tests/fixtures/test-project/pipelines/two-stage.pipeline.yaml +0 -24
  234. package/tests/fixtures/test-project/pipelines/with-startup.pipeline.yaml +0 -19
  235. package/tests/loader.test.ts +0 -385
  236. package/tests/post-validator.test.ts +0 -297
  237. package/tests/repo-resolver.test.ts +0 -102
  238. package/tests/run-store.test.ts +0 -143
  239. package/tests/state-machine.test.ts +0 -110
  240. package/tests/unit/context-propagation.test.ts +0 -45
  241. package/tests/unit/engine.test.ts +0 -770
  242. package/tests/unit/group-loop.test.ts +0 -389
  243. package/tests/unit/group-parallel.test.ts +0 -423
  244. package/tests/unit/state/status-derivation.test.ts +0 -88
  245. package/tsconfig.json +0 -24
  246. package/vitest.config.ts +0 -14
@@ -1,362 +0,0 @@
1
- // Persistence layer for pipeline runs
2
- // Three implementations: InMemoryRunStore (tests), SQLiteRunStore (local), PgRunStore (postgres)
3
-
4
- import { createRequire } from 'node:module';
5
- import type { PipelineRun } from '@studio-foundation/contracts';
6
-
7
- export interface RunStore {
8
- savePipelineRun(run: PipelineRun): void;
9
- getPipelineRun(id: string): PipelineRun | null;
10
- listPipelineRuns(options?: { limit?: number; status?: string }): PipelineRun[];
11
- getLatestRun(pipelineName?: string): PipelineRun | null;
12
- saveLogPath(runId: string, logPath: string): void;
13
- getLogPath(runId: string): string | null;
14
- close?(): void;
15
- }
16
-
17
- export interface AsyncRunStore {
18
- savePipelineRun(run: PipelineRun): Promise<void>;
19
- getPipelineRun(id: string): Promise<PipelineRun | null>;
20
- listPipelineRuns(options?: { limit?: number; status?: string }): Promise<PipelineRun[]>;
21
- getLatestRun(pipelineName?: string): Promise<PipelineRun | null>;
22
- saveLogPath(runId: string, logPath: string): Promise<void>;
23
- getLogPath(runId: string): Promise<string | null>;
24
- close?(): Promise<void>;
25
- }
26
-
27
- export type AnyRunStore = RunStore | AsyncRunStore;
28
-
29
- // In-memory store for tests and simple usage
30
- export class InMemoryRunStore implements RunStore {
31
- private runs: Map<string, PipelineRun> = new Map();
32
- private logPaths: Map<string, string> = new Map();
33
-
34
- savePipelineRun(run: PipelineRun): void {
35
- this.runs.set(run.id, structuredClone(run));
36
- }
37
-
38
- getPipelineRun(id: string): PipelineRun | null {
39
- const run = this.runs.get(id);
40
- return run ? structuredClone(run) : null;
41
- }
42
-
43
- listPipelineRuns(options?: { limit?: number; status?: string }): PipelineRun[] {
44
- let runs = Array.from(this.runs.values());
45
-
46
- if (options?.status) {
47
- runs = runs.filter(r => r.status === options.status);
48
- }
49
-
50
- // Sort by started_at descending
51
- runs.sort((a, b) => b.started_at.localeCompare(a.started_at));
52
-
53
- if (options?.limit) {
54
- runs = runs.slice(0, options.limit);
55
- }
56
-
57
- return runs.map(r => structuredClone(r));
58
- }
59
-
60
- getLatestRun(pipelineName?: string): PipelineRun | null {
61
- let runs = Array.from(this.runs.values());
62
-
63
- if (pipelineName) {
64
- runs = runs.filter(r => r.pipeline_name === pipelineName);
65
- }
66
-
67
- if (runs.length === 0) return null;
68
-
69
- runs.sort((a, b) => b.started_at.localeCompare(a.started_at));
70
- return structuredClone(runs[0]);
71
- }
72
-
73
- saveLogPath(runId: string, logPath: string): void {
74
- this.logPaths.set(runId, logPath);
75
- }
76
-
77
- getLogPath(runId: string): string | null {
78
- return this.logPaths.get(runId) ?? null;
79
- }
80
- }
81
-
82
- // SQLite store for production persistence
83
- // Uses better-sqlite3 (synchronous, simple, no migrations)
84
- // Stores the entire PipelineRun as JSON in a single column — simple, queryable by status
85
- export class SQLiteRunStore implements RunStore {
86
- private db: import('better-sqlite3').Database;
87
-
88
- constructor(dbPath: string) {
89
- const _require = createRequire(import.meta.url);
90
- const Database = _require('better-sqlite3') as new (path: string) => import('better-sqlite3').Database;
91
- this.db = new Database(dbPath);
92
- this.db.pragma('journal_mode = WAL');
93
- this.initSchema();
94
- }
95
-
96
- private initSchema(): void {
97
- this.db.exec(`
98
- CREATE TABLE IF NOT EXISTS pipeline_runs (
99
- id TEXT PRIMARY KEY,
100
- pipeline_name TEXT NOT NULL,
101
- status TEXT NOT NULL,
102
- result TEXT NOT NULL,
103
- started_at TEXT NOT NULL,
104
- completed_at TEXT,
105
- log_path TEXT,
106
- created_at TEXT DEFAULT CURRENT_TIMESTAMP
107
- );
108
-
109
- CREATE INDEX IF NOT EXISTS idx_pipeline_runs_status
110
- ON pipeline_runs(status);
111
- CREATE INDEX IF NOT EXISTS idx_pipeline_runs_created
112
- ON pipeline_runs(created_at DESC);
113
- `);
114
-
115
- // Migration: add log_path column to existing databases
116
- try {
117
- this.db.exec('ALTER TABLE pipeline_runs ADD COLUMN log_path TEXT');
118
- } catch {
119
- // Column already exists — ignore
120
- }
121
-
122
- // Migration: add parent_run_id column to existing databases
123
- try {
124
- this.db.exec('ALTER TABLE pipeline_runs ADD COLUMN parent_run_id TEXT');
125
- this.db.exec('CREATE INDEX IF NOT EXISTS idx_pipeline_runs_parent ON pipeline_runs(parent_run_id)');
126
- } catch {
127
- // Column already exists — ignore
128
- }
129
- }
130
-
131
- savePipelineRun(run: PipelineRun): void {
132
- this.db.prepare(`
133
- INSERT INTO pipeline_runs (id, pipeline_name, status, result, started_at, completed_at, parent_run_id)
134
- VALUES (?, ?, ?, ?, ?, ?, ?)
135
- ON CONFLICT(id) DO UPDATE SET
136
- pipeline_name = excluded.pipeline_name,
137
- status = excluded.status,
138
- result = excluded.result,
139
- started_at = excluded.started_at,
140
- completed_at = excluded.completed_at,
141
- parent_run_id = excluded.parent_run_id
142
- `).run(
143
- run.id,
144
- run.pipeline_name,
145
- run.status,
146
- JSON.stringify(run),
147
- run.started_at,
148
- run.completed_at ?? null,
149
- run.parent_run_id ?? null,
150
- );
151
- }
152
-
153
- getPipelineRun(id: string): PipelineRun | null {
154
- const row = this.db.prepare('SELECT result FROM pipeline_runs WHERE id = ?').get(id) as
155
- | { result: string }
156
- | undefined;
157
-
158
- if (!row) return null;
159
- return JSON.parse(row.result) as PipelineRun;
160
- }
161
-
162
- listPipelineRuns(options?: { limit?: number; status?: string }): PipelineRun[] {
163
- let sql = 'SELECT result FROM pipeline_runs';
164
- const params: unknown[] = [];
165
-
166
- if (options?.status) {
167
- sql += ' WHERE status = ?';
168
- params.push(options.status);
169
- }
170
-
171
- sql += ' ORDER BY created_at DESC';
172
-
173
- if (options?.limit) {
174
- sql += ' LIMIT ?';
175
- params.push(options.limit);
176
- }
177
-
178
- const rows = this.db.prepare(sql).all(...params) as Array<{ result: string }>;
179
- return rows.map(row => JSON.parse(row.result) as PipelineRun);
180
- }
181
-
182
- getLatestRun(pipelineName?: string): PipelineRun | null {
183
- let sql = 'SELECT result FROM pipeline_runs';
184
- const params: unknown[] = [];
185
-
186
- if (pipelineName) {
187
- sql += ' WHERE pipeline_name = ?';
188
- params.push(pipelineName);
189
- }
190
-
191
- sql += ' ORDER BY created_at DESC LIMIT 1';
192
-
193
- const row = this.db.prepare(sql).get(...params) as { result: string } | undefined;
194
- if (!row) return null;
195
- return JSON.parse(row.result) as PipelineRun;
196
- }
197
-
198
- saveLogPath(runId: string, logPath: string): void {
199
- this.db.prepare('UPDATE pipeline_runs SET log_path = ? WHERE id = ?').run(logPath, runId);
200
- }
201
-
202
- getLogPath(runId: string): string | null {
203
- const row = this.db.prepare('SELECT log_path FROM pipeline_runs WHERE id = ?').get(runId) as
204
- | { log_path: string | null }
205
- | undefined;
206
- return row?.log_path ?? null;
207
- }
208
-
209
- close(): void {
210
- this.db.close();
211
- }
212
- }
213
-
214
- // PostgreSQL store — async implementation using raw pg (no Prisma)
215
- // Creates its own `studio_pipeline_runs` table on first use (like SQLiteRunStore does with initSchema).
216
- // Stores the entire PipelineRun as JSON in `result` — same pattern as SQLiteRunStore.
217
- // Uses table prefix `studio_` to avoid conflicts with the user app's own tables.
218
- export class PgRunStore implements AsyncRunStore {
219
- private pool: import('pg').Pool;
220
- private schemaReady: Promise<void> | null = null;
221
-
222
- constructor(connectionString: string) {
223
- const _require = createRequire(import.meta.url);
224
- const { Pool } = _require('pg') as typeof import('pg');
225
- this.pool = new Pool({ connectionString });
226
- }
227
-
228
- private ensureSchema(): Promise<void> {
229
- if (!this.schemaReady) {
230
- this.schemaReady = this.initSchema();
231
- }
232
- return this.schemaReady;
233
- }
234
-
235
- private async initSchema(): Promise<void> {
236
- await this.pool.query(`
237
- CREATE TABLE IF NOT EXISTS studio_pipeline_runs (
238
- id TEXT PRIMARY KEY,
239
- pipeline_name TEXT NOT NULL,
240
- status TEXT NOT NULL,
241
- result TEXT NOT NULL,
242
- started_at TEXT NOT NULL,
243
- completed_at TEXT,
244
- log_path TEXT,
245
- parent_run_id TEXT,
246
- created_at TIMESTAMPTZ DEFAULT NOW()
247
- )
248
- `);
249
- await this.pool.query(`
250
- CREATE INDEX IF NOT EXISTS idx_studio_pipeline_runs_status
251
- ON studio_pipeline_runs(status)
252
- `);
253
- await this.pool.query(`
254
- CREATE INDEX IF NOT EXISTS idx_studio_pipeline_runs_created
255
- ON studio_pipeline_runs(created_at DESC)
256
- `);
257
- await this.pool.query(`
258
- CREATE INDEX IF NOT EXISTS idx_studio_pipeline_runs_parent
259
- ON studio_pipeline_runs(parent_run_id)
260
- `);
261
- }
262
-
263
- async savePipelineRun(run: PipelineRun): Promise<void> {
264
- await this.ensureSchema();
265
- await this.pool.query(
266
- `INSERT INTO studio_pipeline_runs
267
- (id, pipeline_name, status, result, started_at, completed_at, parent_run_id)
268
- VALUES ($1, $2, $3, $4, $5, $6, $7)
269
- ON CONFLICT (id) DO UPDATE SET
270
- pipeline_name = EXCLUDED.pipeline_name,
271
- status = EXCLUDED.status,
272
- result = EXCLUDED.result,
273
- started_at = EXCLUDED.started_at,
274
- completed_at = EXCLUDED.completed_at,
275
- parent_run_id = EXCLUDED.parent_run_id`,
276
- [
277
- run.id,
278
- run.pipeline_name,
279
- run.status,
280
- JSON.stringify(run),
281
- run.started_at,
282
- run.completed_at ?? null,
283
- run.parent_run_id ?? null,
284
- ]
285
- );
286
- }
287
-
288
- async getPipelineRun(id: string): Promise<PipelineRun | null> {
289
- await this.ensureSchema();
290
- const res = await this.pool.query<{ result: string }>(
291
- 'SELECT result FROM studio_pipeline_runs WHERE id = $1',
292
- [id]
293
- );
294
- if (res.rows.length === 0) return null;
295
- return JSON.parse(res.rows[0].result) as PipelineRun;
296
- }
297
-
298
- async listPipelineRuns(options?: { limit?: number; status?: string }): Promise<PipelineRun[]> {
299
- await this.ensureSchema();
300
- const params: unknown[] = [];
301
- let sql = 'SELECT result FROM studio_pipeline_runs';
302
-
303
- if (options?.status) {
304
- params.push(options.status);
305
- sql += ` WHERE status = $${params.length}`;
306
- }
307
-
308
- sql += ' ORDER BY created_at DESC';
309
-
310
- if (options?.limit) {
311
- params.push(options.limit);
312
- sql += ` LIMIT $${params.length}`;
313
- }
314
-
315
- const res = await this.pool.query<{ result: string }>(sql, params);
316
- return res.rows.map((r) => JSON.parse(r.result) as PipelineRun);
317
- }
318
-
319
- async getLatestRun(pipelineName?: string): Promise<PipelineRun | null> {
320
- await this.ensureSchema();
321
- const params: unknown[] = [];
322
- let sql = 'SELECT result FROM studio_pipeline_runs';
323
-
324
- if (pipelineName) {
325
- params.push(pipelineName);
326
- sql += ` WHERE pipeline_name = $${params.length}`;
327
- }
328
-
329
- sql += ' ORDER BY created_at DESC LIMIT 1';
330
-
331
- const res = await this.pool.query<{ result: string }>(sql, params);
332
- if (res.rows.length === 0) return null;
333
- return JSON.parse(res.rows[0].result) as PipelineRun;
334
- }
335
-
336
- async saveLogPath(runId: string, logPath: string): Promise<void> {
337
- await this.ensureSchema();
338
- await this.pool.query(
339
- 'UPDATE studio_pipeline_runs SET log_path = $1 WHERE id = $2',
340
- [logPath, runId]
341
- );
342
- }
343
-
344
- async getLogPath(runId: string): Promise<string | null> {
345
- await this.ensureSchema();
346
- const res = await this.pool.query<{ log_path: string | null }>(
347
- 'SELECT log_path FROM studio_pipeline_runs WHERE id = $1',
348
- [runId]
349
- );
350
- return res.rows[0]?.log_path ?? null;
351
- }
352
-
353
- async close(): Promise<void> {
354
- await this.pool.end();
355
- }
356
-
357
- /** Test-only: wipe table contents. Never call in production. */
358
- async dangerouslyTruncateForTests(): Promise<void> {
359
- await this.ensureSchema();
360
- await this.pool.query('TRUNCATE TABLE studio_pipeline_runs');
361
- }
362
- }
@@ -1,35 +0,0 @@
1
- // Stage lifecycle state machine
2
- // Keeps it simple: linear transitions, no DAG
3
-
4
- export type StageLifecycleState = 'pending' | 'running' | 'success' | 'failed' | 'skipped' | 'rejected' | 'cancelled';
5
-
6
- type StageEvent = 'start' | 'succeed' | 'fail' | 'skip' | 'reject' | 'cancel';
7
-
8
- const VALID_TRANSITIONS: Record<string, StageLifecycleState> = {
9
- 'pending:start': 'running',
10
- 'running:succeed': 'success',
11
- 'running:fail': 'failed',
12
- 'pending:skip': 'skipped',
13
- 'running:reject': 'rejected',
14
- 'running:cancel': 'cancelled',
15
- };
16
-
17
- export function isValidTransition(from: StageLifecycleState, to: StageLifecycleState): boolean {
18
- for (const [key, target] of Object.entries(VALID_TRANSITIONS)) {
19
- if (key.startsWith(`${from}:`) && target === to) {
20
- return true;
21
- }
22
- }
23
- return false;
24
- }
25
-
26
- export function transition(current: StageLifecycleState, event: StageEvent): StageLifecycleState {
27
- const key = `${current}:${event}`;
28
- const next = VALID_TRANSITIONS[key];
29
-
30
- if (!next) {
31
- throw new Error(`Invalid state transition: ${current} + ${event}`);
32
- }
33
-
34
- return next;
35
- }
@@ -1,36 +0,0 @@
1
- // Derive stage status from ralph result
2
- // THIS IS THE CRITICAL FUNCTION
3
- //
4
- // In v7, each stage = 1 ralph call = 1 task.
5
- // This function maps ralph's result directly to stage status.
6
- //
7
- // This was the #1 bug in v6: stage status didn't match task status.
8
- // v7 fix: SIMPLE, DETERMINISTIC, NO MAGIC.
9
-
10
- import type { RalphResult } from '@studio-foundation/ralph';
11
- import type { StageStatus } from '@studio-foundation/contracts';
12
-
13
- /**
14
- * Derives stage status from ralph result.
15
- *
16
- * Rules (simple and exhaustive):
17
- * - ralph 'success' → stage 'success'
18
- * - ralph 'exhausted' → stage 'failed'
19
- * - anything else → error (should never happen)
20
- */
21
- export function deriveStageStatus(ralphResult: RalphResult<unknown>): StageStatus {
22
- if (ralphResult.status === 'success') {
23
- return 'success';
24
- }
25
-
26
- if (ralphResult.status === 'exhausted') {
27
- return 'failed';
28
- }
29
-
30
- if (ralphResult.status === 'cancelled') {
31
- return 'cancelled';
32
- }
33
-
34
- // Should never reach here if RalphResult types are exhaustive
35
- throw new Error(`Unknown ralph status: ${JSON.stringify(ralphResult)}`);
36
- }
@@ -1,113 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import * as fs from 'fs/promises';
3
- import * as path from 'path';
4
- import * as os from 'os';
5
- import { loadContextPacks } from '../src/pipeline/context-pack-loader.js';
6
-
7
- describe('loadContextPacks', () => {
8
- let tmpDir: string;
9
- let workspaceDir: string;
10
-
11
- beforeEach(async () => {
12
- tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'studio-packs-'));
13
- workspaceDir = path.join(tmpDir, 'workspace');
14
- await fs.mkdir(path.join(tmpDir, 'context-packs'), { recursive: true });
15
- await fs.mkdir(workspaceDir, { recursive: true });
16
- });
17
-
18
- afterEach(async () => {
19
- await fs.rm(tmpDir, { recursive: true, force: true });
20
- });
21
-
22
- it('loads a pack with only inline sections', async () => {
23
- await fs.writeFile(
24
- path.join(tmpDir, 'context-packs', 'coding-standards.yaml'),
25
- `name: Coding Standards\ndescription: Our standards\nversion: 1\ninline:\n - title: "Naming"\n content: "Use camelCase"\n - title: "Errors"\n content: "Always catch"`
26
- );
27
-
28
- const result = await loadContextPacks(['coding-standards'], tmpDir);
29
-
30
- expect(result).toHaveLength(1);
31
- expect(result[0].name).toBe('Coding Standards');
32
- expect(result[0].description).toBe('Our standards');
33
- expect(result[0].sections).toHaveLength(2);
34
- expect(result[0].sections[0]).toEqual({ title: 'Naming', content: 'Use camelCase' });
35
- });
36
-
37
- it('loads a pack with file sections read from workspace', async () => {
38
- await fs.writeFile(path.join(workspaceDir, 'STYLE.md'), '# Style Guide\nUse tabs.');
39
- await fs.writeFile(
40
- path.join(tmpDir, 'context-packs', 'style.yaml'),
41
- `name: Style Guide\nversion: 1\nfiles:\n - path: STYLE.md`
42
- );
43
-
44
- const result = await loadContextPacks(['style'], tmpDir, workspaceDir);
45
-
46
- expect(result[0].sections).toHaveLength(1);
47
- expect(result[0].sections[0].title).toBe('STYLE.md');
48
- expect(result[0].sections[0].content).toContain('# Style Guide');
49
- });
50
-
51
- it('puts file sections before inline sections', async () => {
52
- await fs.writeFile(path.join(workspaceDir, 'README.md'), 'Read me.');
53
- await fs.writeFile(
54
- path.join(tmpDir, 'context-packs', 'mixed.yaml'),
55
- `name: Mixed\nversion: 1\nfiles:\n - path: README.md\ninline:\n - title: "Rule"\n content: "Follow rules"`
56
- );
57
-
58
- const result = await loadContextPacks(['mixed'], tmpDir, workspaceDir);
59
-
60
- expect(result[0].sections[0].title).toBe('README.md');
61
- expect(result[0].sections[1].title).toBe('Rule');
62
- });
63
-
64
- it('throws a clear error when pack file does not exist', async () => {
65
- await expect(
66
- loadContextPacks(['nonexistent'], tmpDir)
67
- ).rejects.toThrow(/context pack.*nonexistent.*not found/i);
68
- });
69
-
70
- it('throws a clear error when referenced workspace file does not exist', async () => {
71
- await fs.writeFile(
72
- path.join(tmpDir, 'context-packs', 'bad-pack.yaml'),
73
- `name: Bad Pack\nversion: 1\nfiles:\n - path: missing-file.md`
74
- );
75
-
76
- await expect(
77
- loadContextPacks(['bad-pack'], tmpDir, workspaceDir)
78
- ).rejects.toThrow(/file.*missing-file\.md.*not found/i);
79
- });
80
-
81
- it('throws when files[] referenced but workspacePath not provided', async () => {
82
- await fs.writeFile(
83
- path.join(tmpDir, 'context-packs', 'needs-ws.yaml'),
84
- `name: Needs WS\nversion: 1\nfiles:\n - path: some.md`
85
- );
86
-
87
- await expect(
88
- loadContextPacks(['needs-ws'], tmpDir, undefined)
89
- ).rejects.toThrow(/workspace.*not configured/i);
90
- });
91
-
92
- it('loads multiple packs preserving order', async () => {
93
- await fs.writeFile(
94
- path.join(tmpDir, 'context-packs', 'pack-a.yaml'),
95
- `name: Pack A\nversion: 1\ninline:\n - title: A\n content: a`
96
- );
97
- await fs.writeFile(
98
- path.join(tmpDir, 'context-packs', 'pack-b.yaml'),
99
- `name: Pack B\nversion: 1\ninline:\n - title: B\n content: b`
100
- );
101
-
102
- const result = await loadContextPacks(['pack-a', 'pack-b'], tmpDir);
103
-
104
- expect(result).toHaveLength(2);
105
- expect(result[0].name).toBe('Pack A');
106
- expect(result[1].name).toBe('Pack B');
107
- });
108
-
109
- it('returns empty array when packNames is empty', async () => {
110
- const result = await loadContextPacks([], tmpDir);
111
- expect(result).toEqual([]);
112
- });
113
- });