stageflow 0.8.0 → 0.12.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 (159) hide show
  1. package/README.md +24 -14
  2. package/dist/agent/activity.d.ts +9 -0
  3. package/dist/agent/agentBackend.d.ts +32 -0
  4. package/dist/agent/agentBackend.js +38 -0
  5. package/dist/agent/claudeActivity.d.ts +15 -0
  6. package/dist/agent/claudeActivity.js +58 -0
  7. package/dist/agent/claudeAdapter.d.ts +5 -0
  8. package/dist/agent/claudeAdapter.js +301 -0
  9. package/dist/agent/claudeSession.d.ts +18 -0
  10. package/dist/agent/claudeSession.js +54 -0
  11. package/dist/agent/claudeTools.d.ts +89 -0
  12. package/dist/agent/claudeTools.js +135 -0
  13. package/dist/agent/fakeAgent.js +24 -4
  14. package/dist/agent/piAdapter.d.ts +6 -3
  15. package/dist/agent/piAdapter.js +129 -21
  16. package/dist/agent/port.d.ts +50 -1
  17. package/dist/agent/resolveAgentPort.d.ts +26 -0
  18. package/dist/agent/resolveAgentPort.js +39 -0
  19. package/dist/cli/runCommand.js +2 -2
  20. package/dist/cli/runsCommand.d.ts +22 -0
  21. package/dist/cli/runsCommand.js +773 -0
  22. package/dist/cli/validateOutput.d.ts +2 -2
  23. package/dist/cli/validateOutput.js +2 -2
  24. package/dist/cli.d.ts +1 -1
  25. package/dist/cli.js +43 -5
  26. package/dist/config/browseCatalog.js +1 -1
  27. package/dist/config/createPipeline.d.ts +2 -1
  28. package/dist/config/createPipeline.js +34 -9
  29. package/dist/config/createStage.js +14 -9
  30. package/dist/config/loadPipeline.js +10 -1
  31. package/dist/config/loadStage.js +138 -2
  32. package/dist/config/loadStageflowManifest.js +14 -1
  33. package/dist/config/mergePipelineIncludes.d.ts +1 -0
  34. package/dist/config/mergePipelineIncludes.js +14 -1
  35. package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
  36. package/dist/config/normalizePipelineStageEntry.js +49 -6
  37. package/dist/config/parseCompletionContract.d.ts +8 -0
  38. package/dist/config/parseCompletionContract.js +218 -0
  39. package/dist/config/parsePreEmitChecks.d.ts +3 -0
  40. package/dist/config/parsePreEmitChecks.js +65 -0
  41. package/dist/config/pipelineNeeds.d.ts +19 -0
  42. package/dist/config/pipelineNeeds.js +151 -0
  43. package/dist/config/pipelineStageKeys.js +16 -1
  44. package/dist/config/resolveForkEmitContext.d.ts +6 -2
  45. package/dist/config/resolveForkEmitContext.js +9 -2
  46. package/dist/config/resolvePipelineDag.d.ts +2 -1
  47. package/dist/config/resolvePipelineDag.js +182 -39
  48. package/dist/config/validateCatalog.d.ts +1 -1
  49. package/dist/config/validateCompletionContract.d.ts +4 -0
  50. package/dist/config/validateCompletionContract.js +52 -0
  51. package/dist/envelope/check.d.ts +1 -1
  52. package/dist/envelope/check.js +86 -13
  53. package/dist/envelope/cloneForks.d.ts +1 -1
  54. package/dist/envelope/cloneForks.js +24 -9
  55. package/dist/envelope/feedbackLoop.d.ts +4 -0
  56. package/dist/envelope/feedbackLoop.js +31 -0
  57. package/dist/envelope/payloadSchema.d.ts +7 -1
  58. package/dist/envelope/payloadSchema.js +170 -13
  59. package/dist/envelope/preEmitChecks.d.ts +29 -0
  60. package/dist/envelope/preEmitChecks.js +57 -0
  61. package/dist/hitl/qaTrail.d.ts +25 -0
  62. package/dist/hitl/qaTrail.js +51 -0
  63. package/dist/index.d.ts +1 -1
  64. package/dist/index.js +1 -1
  65. package/dist/mcp/catalogTools.js +4 -2
  66. package/dist/mcp/controlTools.js +72 -0
  67. package/dist/mcp/waitingGates.d.ts +2 -1
  68. package/dist/mcp/waitingGates.js +17 -1
  69. package/dist/package-meta.d.ts +1 -0
  70. package/dist/package-meta.js +1 -0
  71. package/dist/projection/projectRun.d.ts +4 -1
  72. package/dist/projection/projectRun.js +7 -0
  73. package/dist/prompt/feedbackLoopContext.d.ts +2 -0
  74. package/dist/prompt/feedbackLoopContext.js +17 -0
  75. package/dist/prompt/priorEnvelope.d.ts +2 -2
  76. package/dist/prompt/priorEnvelope.js +4 -1
  77. package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
  78. package/dist/runstore/pipelineDagSnapshot.js +51 -11
  79. package/dist/runstore/port.d.ts +185 -3
  80. package/dist/runstore/port.js +42 -3
  81. package/dist/runstore/runProjection.d.ts +7 -3
  82. package/dist/runstore/runProjection.js +30 -15
  83. package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
  84. package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
  85. package/dist/runstore/sqlite/schema.d.ts +1 -1
  86. package/dist/runstore/sqlite/schema.js +99 -0
  87. package/dist/runstore/stageInstanceId.d.ts +1 -1
  88. package/dist/runstore/stageInstanceId.js +3 -2
  89. package/dist/runstore/trackProjection.js +59 -27
  90. package/dist/runstore/verificationHistory.d.ts +19 -0
  91. package/dist/runstore/verificationHistory.js +30 -0
  92. package/dist/runtime/cloneSchedule.d.ts +40 -4
  93. package/dist/runtime/cloneSchedule.js +212 -28
  94. package/dist/runtime/completionCheckRunner.d.ts +137 -0
  95. package/dist/runtime/completionCheckRunner.js +365 -0
  96. package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
  97. package/dist/runtime/completionCheckoutBaseline.js +44 -0
  98. package/dist/runtime/envelopeRouting.d.ts +10 -3
  99. package/dist/runtime/envelopeRouting.js +146 -5
  100. package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
  101. package/dist/runtime/feedbackLoopCoordinator.js +446 -0
  102. package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
  103. package/dist/runtime/feedbackLoopDecision.js +207 -0
  104. package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
  105. package/dist/runtime/feedbackLoopRoute.js +13 -0
  106. package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
  107. package/dist/runtime/feedbackLoopSchedule.js +72 -0
  108. package/dist/runtime/forkGeneration.d.ts +72 -0
  109. package/dist/runtime/forkGeneration.js +295 -0
  110. package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
  111. package/dist/runtime/gitCheckoutCapability.js +97 -0
  112. package/dist/runtime/manualRecoveryState.d.ts +40 -0
  113. package/dist/runtime/manualRecoveryState.js +73 -0
  114. package/dist/runtime/pipelineScheduler.d.ts +13 -1
  115. package/dist/runtime/pipelineScheduler.js +328 -45
  116. package/dist/runtime/replayLifecycle.d.ts +107 -0
  117. package/dist/runtime/replayLifecycle.js +168 -0
  118. package/dist/runtime/runManager.d.ts +61 -1
  119. package/dist/runtime/runManager.js +501 -7
  120. package/dist/runtime/runRetryCoordinator.d.ts +8 -1
  121. package/dist/runtime/runRetryCoordinator.js +47 -10
  122. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
  123. package/dist/runtime/stageAttemptBootstrap.js +79 -1
  124. package/dist/runtime/stageProcessLauncher.d.ts +1 -1
  125. package/dist/runtime/stageRecovery.js +1 -1
  126. package/dist/runtime/stageRunner.d.ts +4 -1
  127. package/dist/runtime/stageRunner.js +29 -3
  128. package/dist/runtime/stageWorker.js +69 -2
  129. package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
  130. package/dist/runtime/verifiedStageExecution.d.ts +29 -0
  131. package/dist/runtime/verifiedStageExecution.js +119 -0
  132. package/dist/server/bootstrap.js +1 -0
  133. package/dist/server/http.js +114 -0
  134. package/dist/server/operatorResults.js +2 -0
  135. package/dist/tools/askOperator.d.ts +161 -5
  136. package/dist/tools/askOperator.js +58 -25
  137. package/dist/tools/emitStageEnvelope.d.ts +12 -12
  138. package/dist/tools/emitStageEnvelope.js +112 -24
  139. package/dist/types/completion.d.ts +43 -0
  140. package/dist/types/completion.js +1 -0
  141. package/dist/types/envelope.d.ts +18 -0
  142. package/dist/types/forkChoice.d.ts +9 -4
  143. package/dist/types/forkChoice.js +1 -1
  144. package/dist/types/pipeline.d.ts +39 -2
  145. package/dist/types/preEmitCheck.d.ts +18 -0
  146. package/dist/types/preEmitCheck.js +1 -0
  147. package/dist/types/stage.d.ts +13 -0
  148. package/dist/types/stageflowManifest.d.ts +2 -0
  149. package/dist/ui/assets/index-CXGz4nL7.js +118 -0
  150. package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
  151. package/dist/ui/index.html +2 -2
  152. package/package.json +2 -1
  153. package/skills/stageflow/references/control-surface.md +4 -2
  154. package/skills/stageflow-author/references/catalog-mapping.md +26 -2
  155. package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
  156. package/skills/stageflow-run/SKILL.md +56 -25
  157. package/skills/stageflow-run/references/mcp-call.md +2 -2
  158. package/skills/stageflow-run/references/native-question-ui.md +78 -0
  159. package/dist/ui/assets/index-CSoGDA3A.js +0 -118
@@ -59,6 +59,7 @@ CREATE TABLE IF NOT EXISTS stage_executions (
59
59
  stage_id TEXT NOT NULL,
60
60
  attempt INTEGER NOT NULL,
61
61
  status TEXT NOT NULL,
62
+ verification_outcome TEXT NOT NULL DEFAULT 'not_run',
62
63
  started_at TEXT,
63
64
  finished_at TEXT,
64
65
  envelope_json TEXT,
@@ -69,6 +70,46 @@ CREATE INDEX IF NOT EXISTS idx_stage_executions_run_stage
69
70
  ON stage_executions (run_id, stage_id, attempt);
70
71
  `);
71
72
  }
73
+ function ensureStageExecutionVerificationOutcomeColumn(db) {
74
+ const cols = db
75
+ .prepare(`PRAGMA table_info(stage_executions)`)
76
+ .all();
77
+ if (!cols.some((c) => c.name === "verification_outcome")) {
78
+ db.exec(`ALTER TABLE stage_executions ADD COLUMN verification_outcome TEXT NOT NULL DEFAULT 'not_run'`);
79
+ }
80
+ }
81
+ function backfillVerificationOutcomes(db) {
82
+ db.exec(`
83
+ UPDATE stage_executions
84
+ SET verification_outcome = CASE
85
+ WHEN status = 'succeeded'
86
+ AND EXISTS (
87
+ SELECT 1 FROM verification_check_results AS check_result
88
+ WHERE check_result.run_id = stage_executions.run_id
89
+ AND check_result.stage_id = stage_executions.stage_id
90
+ AND check_result.attempt = stage_executions.attempt
91
+ )
92
+ AND NOT EXISTS (
93
+ SELECT 1 FROM verification_check_results AS check_result
94
+ WHERE check_result.run_id = stage_executions.run_id
95
+ AND check_result.stage_id = stage_executions.stage_id
96
+ AND check_result.attempt = stage_executions.attempt
97
+ AND check_result.status != 'passed'
98
+ )
99
+ THEN 'passed'
100
+ WHEN status = 'failed'
101
+ AND EXISTS (
102
+ SELECT 1 FROM verification_check_results AS check_result
103
+ WHERE check_result.run_id = stage_executions.run_id
104
+ AND check_result.stage_id = stage_executions.stage_id
105
+ AND check_result.attempt = stage_executions.attempt
106
+ )
107
+ THEN 'failed'
108
+ ELSE verification_outcome
109
+ END
110
+ WHERE verification_outcome = 'not_run';
111
+ `);
112
+ }
72
113
  function ensureStageEventsAttemptColumn(db) {
73
114
  const cols = db
74
115
  .prepare(`PRAGMA table_info(stage_events)`)
@@ -81,12 +122,63 @@ CREATE INDEX IF NOT EXISTS idx_stage_events_run_stage_attempt_at
81
122
  `);
82
123
  }
83
124
  }
125
+ function ensureVerificationCheckResultsTable(db) {
126
+ db.exec(`
127
+ CREATE TABLE IF NOT EXISTS verification_check_results (
128
+ run_id TEXT NOT NULL,
129
+ stage_id TEXT NOT NULL,
130
+ attempt INTEGER NOT NULL,
131
+ check_id TEXT NOT NULL,
132
+ check_type TEXT NOT NULL,
133
+ status TEXT NOT NULL,
134
+ started_at TEXT,
135
+ finished_at TEXT,
136
+ evidence_json TEXT,
137
+ PRIMARY KEY (run_id, stage_id, attempt, check_id),
138
+ FOREIGN KEY (run_id, stage_id, attempt)
139
+ REFERENCES stage_executions(run_id, stage_id, attempt)
140
+ );
141
+ CREATE INDEX IF NOT EXISTS idx_verification_check_results_execution
142
+ ON verification_check_results (run_id, stage_id, attempt, check_id);
143
+ `);
144
+ }
145
+ function ensureFeedbackReplayStagePassEnvelopeColumn(db) {
146
+ const cols = db
147
+ .prepare(`PRAGMA table_info(feedback_replay_stage_passes)`)
148
+ .all();
149
+ if (cols.length === 0)
150
+ return;
151
+ if (!cols.some((c) => c.name === "emitted_envelope_json")) {
152
+ db.exec(`ALTER TABLE feedback_replay_stage_passes ADD COLUMN emitted_envelope_json TEXT`);
153
+ }
154
+ }
155
+ function ensureFeedbackReplayStagePassSessionOriginColumn(db) {
156
+ const cols = db
157
+ .prepare(`PRAGMA table_info(feedback_replay_stage_passes)`)
158
+ .all();
159
+ if (cols.length === 0)
160
+ return;
161
+ if (!cols.some((c) => c.name === "session_origin_attempt")) {
162
+ db.exec(`ALTER TABLE feedback_replay_stage_passes ADD COLUMN session_origin_attempt INTEGER`);
163
+ }
164
+ }
165
+ function ensureFeedbackLoopDeferredSendBackColumn(db) {
166
+ const cols = db
167
+ .prepare(`PRAGMA table_info(feedback_loops)`)
168
+ .all();
169
+ if (cols.length === 0)
170
+ return;
171
+ if (!cols.some((c) => c.name === "deferred_send_back_json")) {
172
+ db.exec(`ALTER TABLE feedback_loops ADD COLUMN deferred_send_back_json TEXT`);
173
+ }
174
+ }
84
175
  function executionFromRow(row) {
85
176
  return {
86
177
  run_id: row.run_id,
87
178
  stage_id: row.stage_id,
88
179
  attempt: row.attempt,
89
180
  status: row.status,
181
+ verification_outcome: row.verification_outcome ?? "not_run",
90
182
  ...(row.started_at != null ? { started_at: row.started_at } : {}),
91
183
  ...(row.finished_at != null ? { finished_at: row.finished_at } : {}),
92
184
  envelope: row.envelope_json
@@ -94,6 +186,95 @@ function executionFromRow(row) {
94
186
  : null,
95
187
  };
96
188
  }
189
+ function verificationCheckResultFromRow(row) {
190
+ return {
191
+ run_id: row.run_id,
192
+ stage_id: row.stage_id,
193
+ attempt: row.attempt,
194
+ check_id: row.check_id,
195
+ check_type: row.check_type,
196
+ status: row.status,
197
+ ...(row.started_at != null ? { started_at: row.started_at } : {}),
198
+ ...(row.finished_at != null ? { finished_at: row.finished_at } : {}),
199
+ ...(row.evidence_json != null
200
+ ? { evidence: JSON.parse(row.evidence_json) }
201
+ : {}),
202
+ };
203
+ }
204
+ function feedbackLoopFromRow(row) {
205
+ return {
206
+ run_id: row.run_id,
207
+ loop_id: row.loop_id,
208
+ source_stage_id: row.source_stage_id,
209
+ source_attempt: row.source_attempt,
210
+ policy: JSON.parse(row.policy_json),
211
+ state: row.state,
212
+ ...(row.current_replay_id != null
213
+ ? { current_replay_id: row.current_replay_id }
214
+ : {}),
215
+ ...(row.current_replay_number != null
216
+ ? { current_replay_number: row.current_replay_number }
217
+ : {}),
218
+ ...(row.deferred_send_back_json != null
219
+ ? {
220
+ deferred_send_back: JSON.parse(row.deferred_send_back_json),
221
+ }
222
+ : {}),
223
+ created_at: row.created_at,
224
+ updated_at: row.updated_at,
225
+ };
226
+ }
227
+ function feedbackReplayFromRow(row) {
228
+ return {
229
+ run_id: row.run_id,
230
+ replay_id: row.replay_id,
231
+ loop_id: row.loop_id,
232
+ source_stage_id: row.source_stage_id,
233
+ source_attempt: row.source_attempt,
234
+ target_stage_id: row.target_stage_id,
235
+ replay_number: row.replay_number,
236
+ max_replays: row.max_replays,
237
+ replay_session: row.replay_session,
238
+ route_stage_ids: JSON.parse(row.route_stage_ids_json),
239
+ feedback_envelope: JSON.parse(row.feedback_envelope_json),
240
+ status: row.status,
241
+ created_at: row.created_at,
242
+ updated_at: row.updated_at,
243
+ };
244
+ }
245
+ function feedbackReplayStagePassFromRow(row) {
246
+ return {
247
+ run_id: row.run_id,
248
+ replay_id: row.replay_id,
249
+ stage_id: row.stage_id,
250
+ stage_attempt: row.stage_attempt,
251
+ ...(row.session_origin_attempt != null
252
+ ? { session_origin_attempt: row.session_origin_attempt }
253
+ : {}),
254
+ session_mode: row.session_mode,
255
+ status: row.status,
256
+ ...(row.started_at != null ? { started_at: row.started_at } : {}),
257
+ ...(row.finished_at != null ? { finished_at: row.finished_at } : {}),
258
+ ...(row.emitted_envelope_json != null
259
+ ? {
260
+ emitted_envelope: JSON.parse(row.emitted_envelope_json),
261
+ }
262
+ : {}),
263
+ };
264
+ }
265
+ function forkGenerationFromRow(row) {
266
+ return {
267
+ run_id: row.run_id,
268
+ generation_id: row.generation_id,
269
+ ...(row.replay_id != null ? { replay_id: row.replay_id } : {}),
270
+ fork_parent_stage_id: row.fork_parent_stage_id,
271
+ generation_number: row.generation_number,
272
+ clone_stage_ids: JSON.parse(row.clone_stage_ids_json),
273
+ status: row.status,
274
+ created_at: row.created_at,
275
+ updated_at: row.updated_at,
276
+ };
277
+ }
97
278
  export class SqliteRunStore {
98
279
  storeRoot;
99
280
  db;
@@ -110,7 +291,13 @@ export class SqliteRunStore {
110
291
  ensurePipelineDagColumn(this.db);
111
292
  ensureRunLocatorColumns(this.db);
112
293
  ensureStageExecutionsTable(this.db);
294
+ ensureStageExecutionVerificationOutcomeColumn(this.db);
113
295
  ensureStageEventsAttemptColumn(this.db);
296
+ ensureVerificationCheckResultsTable(this.db);
297
+ ensureFeedbackReplayStagePassEnvelopeColumn(this.db);
298
+ ensureFeedbackReplayStagePassSessionOriginColumn(this.db);
299
+ ensureFeedbackLoopDeferredSendBackColumn(this.db);
300
+ backfillVerificationOutcomes(this.db);
114
301
  this.migratePromise = importDiskRunsIfEmpty(this.db, storeRoot).then(() => undefined);
115
302
  }
116
303
  /** Await one-shot disk import before catalog reads (create paths also wait). */
@@ -235,16 +422,17 @@ export class SqliteRunStore {
235
422
  .get(runId, stageId);
236
423
  const attempt = (maxRow?.max_attempt ?? 0) + 1;
237
424
  this.db
238
- .prepare(`INSERT INTO stage_executions (run_id, stage_id, attempt, status)
239
- VALUES (@run_id, @stage_id, @attempt, @status)`)
425
+ .prepare(`INSERT INTO stage_executions (run_id, stage_id, attempt, status, verification_outcome)
426
+ VALUES (@run_id, @stage_id, @attempt, @status, @verification_outcome)`)
240
427
  .run({
241
428
  run_id: runId,
242
429
  stage_id: stageId,
243
430
  attempt,
244
431
  status: "pending",
432
+ verification_outcome: "not_run",
245
433
  });
246
434
  const row = this.db
247
- .prepare(`SELECT run_id, stage_id, attempt, status, started_at, finished_at, envelope_json
435
+ .prepare(`SELECT run_id, stage_id, attempt, status, verification_outcome, started_at, finished_at, envelope_json
248
436
  FROM stage_executions
249
437
  WHERE run_id = ? AND stage_id = ? AND attempt = ?`)
250
438
  .get(runId, stageId, attempt);
@@ -254,7 +442,7 @@ export class SqliteRunStore {
254
442
  async listStageExecutions(runId, stageId) {
255
443
  await this.ready();
256
444
  const rows = this.db
257
- .prepare(`SELECT run_id, stage_id, attempt, status, started_at, finished_at, envelope_json
445
+ .prepare(`SELECT run_id, stage_id, attempt, status, verification_outcome, started_at, finished_at, envelope_json
258
446
  FROM stage_executions
259
447
  WHERE run_id = ? AND stage_id = ?
260
448
  ORDER BY attempt ASC`)
@@ -264,7 +452,7 @@ export class SqliteRunStore {
264
452
  async getLatestStageExecution(runId, stageId) {
265
453
  await this.ready();
266
454
  const row = this.db
267
- .prepare(`SELECT run_id, stage_id, attempt, status, started_at, finished_at, envelope_json
455
+ .prepare(`SELECT run_id, stage_id, attempt, status, verification_outcome, started_at, finished_at, envelope_json
268
456
  FROM stage_executions
269
457
  WHERE run_id = ? AND stage_id = ?
270
458
  ORDER BY attempt DESC
@@ -283,7 +471,7 @@ export class SqliteRunStore {
283
471
  async getStageExecution(runId, stageId, attempt) {
284
472
  await this.ready();
285
473
  const row = this.db
286
- .prepare(`SELECT run_id, stage_id, attempt, status, started_at, finished_at, envelope_json
474
+ .prepare(`SELECT run_id, stage_id, attempt, status, verification_outcome, started_at, finished_at, envelope_json
287
475
  FROM stage_executions
288
476
  WHERE run_id = ? AND stage_id = ? AND attempt = ?`)
289
477
  .get(runId, stageId, attempt);
@@ -304,6 +492,10 @@ export class SqliteRunStore {
304
492
  sets.push("status = @status");
305
493
  params.status = patch.status;
306
494
  }
495
+ if (patch.verification_outcome !== undefined) {
496
+ sets.push("verification_outcome = @verification_outcome");
497
+ params.verification_outcome = patch.verification_outcome;
498
+ }
307
499
  if (patch.started_at !== undefined) {
308
500
  sets.push("started_at = @started_at");
309
501
  params.started_at = patch.started_at;
@@ -327,6 +519,50 @@ export class SqliteRunStore {
327
519
  throw new Error(`Stage execution not found: ${runId}/${stageId} attempt ${attempt}`);
328
520
  }
329
521
  }
522
+ async upsertVerificationCheckResult(runId, stageId, result, options) {
523
+ await this.ready();
524
+ const attempt = options?.attempt ?? 1;
525
+ await this.getStageExecution(runId, stageId, attempt);
526
+ this.db
527
+ .prepare(`INSERT INTO verification_check_results
528
+ (run_id, stage_id, attempt, check_id, check_type, status, started_at, finished_at, evidence_json)
529
+ VALUES
530
+ (@run_id, @stage_id, @attempt, @check_id, @check_type, @status, @started_at, @finished_at, @evidence_json)
531
+ ON CONFLICT(run_id, stage_id, attempt, check_id) DO UPDATE SET
532
+ check_type = excluded.check_type,
533
+ status = excluded.status,
534
+ started_at = COALESCE(excluded.started_at, verification_check_results.started_at),
535
+ finished_at = COALESCE(excluded.finished_at, verification_check_results.finished_at),
536
+ evidence_json = COALESCE(excluded.evidence_json, verification_check_results.evidence_json)`)
537
+ .run({
538
+ run_id: runId,
539
+ stage_id: stageId,
540
+ attempt,
541
+ check_id: result.check_id,
542
+ check_type: result.check_type,
543
+ status: result.status,
544
+ started_at: result.started_at ?? null,
545
+ finished_at: result.finished_at ?? null,
546
+ evidence_json: result.evidence !== undefined ? JSON.stringify(result.evidence) : null,
547
+ });
548
+ }
549
+ async listVerificationCheckResults(runId, stageId, attempt) {
550
+ await this.ready();
551
+ const rows = attempt === undefined
552
+ ? this.db
553
+ .prepare(`SELECT run_id, stage_id, attempt, check_id, check_type, status, started_at, finished_at, evidence_json
554
+ FROM verification_check_results
555
+ WHERE run_id = ? AND stage_id = ?
556
+ ORDER BY attempt ASC, rowid ASC`)
557
+ .all(runId, stageId)
558
+ : this.db
559
+ .prepare(`SELECT run_id, stage_id, attempt, check_id, check_type, status, started_at, finished_at, evidence_json
560
+ FROM verification_check_results
561
+ WHERE run_id = ? AND stage_id = ? AND attempt = ?
562
+ ORDER BY rowid ASC`)
563
+ .all(runId, stageId, attempt);
564
+ return rows.map(verificationCheckResultFromRow);
565
+ }
330
566
  async writeEnvelope(runId, stageId, envelope, options) {
331
567
  const attempt = options?.attempt ?? 1;
332
568
  await this.ensureAttemptWorkspace(runId, stageId, attempt);
@@ -451,7 +687,10 @@ export class SqliteRunStore {
451
687
  for (const row of rows) {
452
688
  const dagSnapshot = this.readPipelineDagSnapshotFromRow(row);
453
689
  const stages = await this.loadStageSnapshots(row.run_id, dagSnapshot?.stage_ids);
454
- summaries.push(projectRunSummary(this.runMetaFromRow(row), stages));
690
+ const { active } = await this.loadFeedbackLoopHistory(row.run_id);
691
+ summaries.push(projectRunSummary(this.runMetaFromRow(row), stages, {
692
+ active_feedback_loop: active,
693
+ }));
455
694
  }
456
695
  return summaries;
457
696
  }
@@ -460,7 +699,386 @@ export class SqliteRunStore {
460
699
  const row = this.getRunRow(runId);
461
700
  const dagSnapshot = this.readPipelineDagSnapshotFromRow(row);
462
701
  const stages = await this.loadStageSnapshots(runId, dagSnapshot?.stage_ids);
463
- return projectRunDetail(this.runMetaFromRow(row), stages, row.task_yaml, dagSnapshot);
702
+ const { history, active } = await this.loadFeedbackLoopHistory(runId);
703
+ return projectRunDetail(this.runMetaFromRow(row), stages, row.task_yaml, dagSnapshot, {
704
+ feedback_loops: history,
705
+ active_feedback_loop: active,
706
+ });
707
+ }
708
+ async createFeedbackLoop(runId, input) {
709
+ await this.ready();
710
+ this.getRunRow(runId);
711
+ const now = new Date().toISOString();
712
+ const state = input.state ?? "active";
713
+ this.db
714
+ .prepare(`INSERT INTO feedback_loops
715
+ (run_id, loop_id, source_stage_id, source_attempt, policy_json, state,
716
+ current_replay_id, current_replay_number, created_at, updated_at)
717
+ VALUES
718
+ (@run_id, @loop_id, @source_stage_id, @source_attempt, @policy_json, @state,
719
+ NULL, NULL, @created_at, @updated_at)`)
720
+ .run({
721
+ run_id: runId,
722
+ loop_id: input.loop_id,
723
+ source_stage_id: input.source_stage_id,
724
+ source_attempt: input.source_attempt,
725
+ policy_json: JSON.stringify(input.policy),
726
+ state,
727
+ created_at: now,
728
+ updated_at: now,
729
+ });
730
+ return this.getFeedbackLoop(runId, input.loop_id);
731
+ }
732
+ async getFeedbackLoop(runId, loopId) {
733
+ await this.ready();
734
+ const row = this.db
735
+ .prepare(`SELECT run_id, loop_id, source_stage_id, source_attempt, policy_json, state,
736
+ current_replay_id, current_replay_number, deferred_send_back_json,
737
+ created_at, updated_at
738
+ FROM feedback_loops WHERE run_id = ? AND loop_id = ?`)
739
+ .get(runId, loopId);
740
+ if (!row) {
741
+ throw new Error(`Feedback loop not found: ${runId}/${loopId}`);
742
+ }
743
+ return feedbackLoopFromRow(row);
744
+ }
745
+ async listFeedbackLoops(runId) {
746
+ await this.ready();
747
+ this.getRunRow(runId);
748
+ const rows = this.db
749
+ .prepare(`SELECT run_id, loop_id, source_stage_id, source_attempt, policy_json, state,
750
+ current_replay_id, current_replay_number, deferred_send_back_json,
751
+ created_at, updated_at
752
+ FROM feedback_loops WHERE run_id = ? ORDER BY created_at ASC`)
753
+ .all(runId);
754
+ return rows.map(feedbackLoopFromRow);
755
+ }
756
+ async updateFeedbackLoop(runId, loopId, patch, options) {
757
+ await this.ready();
758
+ const sets = ["updated_at = @updated_at"];
759
+ const params = {
760
+ run_id: runId,
761
+ loop_id: loopId,
762
+ updated_at: new Date().toISOString(),
763
+ };
764
+ if (patch.state !== undefined) {
765
+ sets.push("state = @state");
766
+ params.state = patch.state;
767
+ }
768
+ if (patch.current_replay_id !== undefined) {
769
+ sets.push("current_replay_id = @current_replay_id");
770
+ params.current_replay_id = patch.current_replay_id;
771
+ }
772
+ if (patch.current_replay_number !== undefined) {
773
+ sets.push("current_replay_number = @current_replay_number");
774
+ params.current_replay_number = patch.current_replay_number;
775
+ }
776
+ if (patch.policy !== undefined) {
777
+ sets.push("policy_json = @policy_json");
778
+ params.policy_json = JSON.stringify(patch.policy);
779
+ }
780
+ if (patch.deferred_send_back !== undefined) {
781
+ sets.push("deferred_send_back_json = @deferred_send_back_json");
782
+ params.deferred_send_back_json =
783
+ patch.deferred_send_back === null
784
+ ? null
785
+ : JSON.stringify(patch.deferred_send_back);
786
+ }
787
+ const whereParts = ["run_id = @run_id", "loop_id = @loop_id"];
788
+ if (options?.expectedState !== undefined) {
789
+ whereParts.push("state = @expected_state");
790
+ params.expected_state = options.expectedState;
791
+ }
792
+ const result = this.db
793
+ .prepare(`UPDATE feedback_loops SET ${sets.join(", ")}
794
+ WHERE ${whereParts.join(" AND ")}`)
795
+ .run(params);
796
+ if (result.changes === 0) {
797
+ if (options?.expectedState !== undefined) {
798
+ const exists = this.db
799
+ .prepare(`SELECT 1 AS ok FROM feedback_loops WHERE run_id = ? AND loop_id = ?`)
800
+ .get(runId, loopId);
801
+ if (exists === undefined) {
802
+ throw new Error(`Feedback loop not found: ${runId}/${loopId}`);
803
+ }
804
+ return false;
805
+ }
806
+ throw new Error(`Feedback loop not found: ${runId}/${loopId}`);
807
+ }
808
+ return true;
809
+ }
810
+ async createFeedbackReplay(runId, input) {
811
+ await this.ready();
812
+ await this.getFeedbackLoop(runId, input.loop_id);
813
+ const now = new Date().toISOString();
814
+ const status = input.status ?? "scheduled";
815
+ this.db
816
+ .prepare(`INSERT INTO feedback_replays
817
+ (run_id, replay_id, loop_id, source_stage_id, source_attempt, target_stage_id,
818
+ replay_number, max_replays, replay_session, route_stage_ids_json,
819
+ feedback_envelope_json, status, created_at, updated_at)
820
+ VALUES
821
+ (@run_id, @replay_id, @loop_id, @source_stage_id, @source_attempt, @target_stage_id,
822
+ @replay_number, @max_replays, @replay_session, @route_stage_ids_json,
823
+ @feedback_envelope_json, @status, @created_at, @updated_at)`)
824
+ .run({
825
+ run_id: runId,
826
+ replay_id: input.replay_id,
827
+ loop_id: input.loop_id,
828
+ source_stage_id: input.source_stage_id,
829
+ source_attempt: input.source_attempt,
830
+ target_stage_id: input.target_stage_id,
831
+ replay_number: input.replay_number,
832
+ max_replays: input.max_replays,
833
+ replay_session: input.replay_session,
834
+ route_stage_ids_json: JSON.stringify(input.route_stage_ids),
835
+ feedback_envelope_json: JSON.stringify(input.feedback_envelope),
836
+ status,
837
+ created_at: now,
838
+ updated_at: now,
839
+ });
840
+ return this.getFeedbackReplay(runId, input.replay_id);
841
+ }
842
+ async getFeedbackReplay(runId, replayId) {
843
+ await this.ready();
844
+ const row = this.db
845
+ .prepare(`SELECT run_id, replay_id, loop_id, source_stage_id, source_attempt, target_stage_id,
846
+ replay_number, max_replays, replay_session, route_stage_ids_json,
847
+ feedback_envelope_json, status, created_at, updated_at
848
+ FROM feedback_replays WHERE run_id = ? AND replay_id = ?`)
849
+ .get(runId, replayId);
850
+ if (!row) {
851
+ throw new Error(`Feedback replay not found: ${runId}/${replayId}`);
852
+ }
853
+ return feedbackReplayFromRow(row);
854
+ }
855
+ async listFeedbackReplays(runId, loopId) {
856
+ await this.ready();
857
+ await this.getFeedbackLoop(runId, loopId);
858
+ const rows = this.db
859
+ .prepare(`SELECT run_id, replay_id, loop_id, source_stage_id, source_attempt, target_stage_id,
860
+ replay_number, max_replays, replay_session, route_stage_ids_json,
861
+ feedback_envelope_json, status, created_at, updated_at
862
+ FROM feedback_replays
863
+ WHERE run_id = ? AND loop_id = ?
864
+ ORDER BY replay_number ASC`)
865
+ .all(runId, loopId);
866
+ return rows.map(feedbackReplayFromRow);
867
+ }
868
+ async updateFeedbackReplay(runId, replayId, patch) {
869
+ await this.ready();
870
+ const sets = ["updated_at = @updated_at"];
871
+ const params = {
872
+ run_id: runId,
873
+ replay_id: replayId,
874
+ updated_at: new Date().toISOString(),
875
+ };
876
+ if (patch.status !== undefined) {
877
+ sets.push("status = @status");
878
+ params.status = patch.status;
879
+ }
880
+ const result = this.db
881
+ .prepare(`UPDATE feedback_replays SET ${sets.join(", ")}
882
+ WHERE run_id = @run_id AND replay_id = @replay_id`)
883
+ .run(params);
884
+ if (result.changes === 0) {
885
+ throw new Error(`Feedback replay not found: ${runId}/${replayId}`);
886
+ }
887
+ }
888
+ async createFeedbackReplayStagePass(runId, input) {
889
+ await this.ready();
890
+ await this.getFeedbackReplay(runId, input.replay_id);
891
+ const status = input.status ?? "pending";
892
+ this.db
893
+ .prepare(`INSERT INTO feedback_replay_stage_passes
894
+ (run_id, replay_id, stage_id, stage_attempt, session_origin_attempt,
895
+ session_mode, status, started_at, finished_at, emitted_envelope_json)
896
+ VALUES
897
+ (@run_id, @replay_id, @stage_id, @stage_attempt, @session_origin_attempt,
898
+ @session_mode, @status, @started_at, @finished_at, @emitted_envelope_json)`)
899
+ .run({
900
+ run_id: runId,
901
+ replay_id: input.replay_id,
902
+ stage_id: input.stage_id,
903
+ stage_attempt: input.stage_attempt,
904
+ session_origin_attempt: input.session_origin_attempt ?? null,
905
+ session_mode: input.session_mode,
906
+ status,
907
+ started_at: input.started_at ?? null,
908
+ finished_at: input.finished_at ?? null,
909
+ emitted_envelope_json: input.emitted_envelope !== undefined
910
+ ? JSON.stringify(input.emitted_envelope)
911
+ : null,
912
+ });
913
+ const row = this.db
914
+ .prepare(`SELECT run_id, replay_id, stage_id, stage_attempt, session_origin_attempt,
915
+ session_mode, status, started_at, finished_at, emitted_envelope_json
916
+ FROM feedback_replay_stage_passes
917
+ WHERE run_id = ? AND replay_id = ? AND stage_id = ?`)
918
+ .get(runId, input.replay_id, input.stage_id);
919
+ return feedbackReplayStagePassFromRow(row);
920
+ }
921
+ async listFeedbackReplayStagePasses(runId, replayId) {
922
+ await this.ready();
923
+ await this.getFeedbackReplay(runId, replayId);
924
+ const rows = this.db
925
+ .prepare(`SELECT run_id, replay_id, stage_id, stage_attempt, session_origin_attempt,
926
+ session_mode, status, started_at, finished_at, emitted_envelope_json
927
+ FROM feedback_replay_stage_passes
928
+ WHERE run_id = ? AND replay_id = ?
929
+ ORDER BY stage_id ASC`)
930
+ .all(runId, replayId);
931
+ return rows.map(feedbackReplayStagePassFromRow);
932
+ }
933
+ async updateFeedbackReplayStagePass(runId, replayId, stageId, patch) {
934
+ await this.ready();
935
+ const sets = [];
936
+ const params = {
937
+ run_id: runId,
938
+ replay_id: replayId,
939
+ stage_id: stageId,
940
+ };
941
+ if (patch.status !== undefined) {
942
+ sets.push("status = @status");
943
+ params.status = patch.status;
944
+ }
945
+ if (patch.stage_attempt !== undefined) {
946
+ sets.push("stage_attempt = @stage_attempt");
947
+ params.stage_attempt = patch.stage_attempt;
948
+ }
949
+ if (patch.started_at !== undefined) {
950
+ sets.push("started_at = @started_at");
951
+ params.started_at = patch.started_at;
952
+ }
953
+ if (patch.finished_at !== undefined) {
954
+ sets.push("finished_at = @finished_at");
955
+ params.finished_at = patch.finished_at;
956
+ }
957
+ if (patch.emitted_envelope !== undefined) {
958
+ sets.push("emitted_envelope_json = @emitted_envelope_json");
959
+ params.emitted_envelope_json =
960
+ patch.emitted_envelope === null
961
+ ? null
962
+ : JSON.stringify(patch.emitted_envelope);
963
+ }
964
+ if (sets.length === 0)
965
+ return;
966
+ const result = this.db
967
+ .prepare(`UPDATE feedback_replay_stage_passes SET ${sets.join(", ")}
968
+ WHERE run_id = @run_id AND replay_id = @replay_id AND stage_id = @stage_id`)
969
+ .run(params);
970
+ if (result.changes === 0) {
971
+ throw new Error(`Feedback replay stage pass not found: ${runId}/${replayId}/${stageId}`);
972
+ }
973
+ }
974
+ async createForkGeneration(runId, input) {
975
+ await this.ready();
976
+ this.getRunRow(runId);
977
+ if (input.replay_id !== undefined) {
978
+ await this.getFeedbackReplay(runId, input.replay_id);
979
+ }
980
+ const now = new Date().toISOString();
981
+ const status = input.status ?? "active";
982
+ this.db
983
+ .prepare(`INSERT INTO fork_generations
984
+ (run_id, generation_id, replay_id, fork_parent_stage_id, generation_number,
985
+ clone_stage_ids_json, status, created_at, updated_at)
986
+ VALUES
987
+ (@run_id, @generation_id, @replay_id, @fork_parent_stage_id, @generation_number,
988
+ @clone_stage_ids_json, @status, @created_at, @updated_at)`)
989
+ .run({
990
+ run_id: runId,
991
+ generation_id: input.generation_id,
992
+ replay_id: input.replay_id ?? null,
993
+ fork_parent_stage_id: input.fork_parent_stage_id,
994
+ generation_number: input.generation_number,
995
+ clone_stage_ids_json: JSON.stringify(input.clone_stage_ids),
996
+ status,
997
+ created_at: now,
998
+ updated_at: now,
999
+ });
1000
+ const row = this.db
1001
+ .prepare(`SELECT run_id, generation_id, replay_id, fork_parent_stage_id, generation_number,
1002
+ clone_stage_ids_json, status, created_at, updated_at
1003
+ FROM fork_generations WHERE run_id = ? AND generation_id = ?`)
1004
+ .get(runId, input.generation_id);
1005
+ return forkGenerationFromRow(row);
1006
+ }
1007
+ async listForkGenerations(runId, options) {
1008
+ await this.ready();
1009
+ this.getRunRow(runId);
1010
+ const clauses = ["run_id = ?"];
1011
+ const params = [runId];
1012
+ if (options?.replayId !== undefined) {
1013
+ clauses.push("replay_id = ?");
1014
+ params.push(options.replayId);
1015
+ }
1016
+ if (options?.forkParentStageId !== undefined) {
1017
+ clauses.push("fork_parent_stage_id = ?");
1018
+ params.push(options.forkParentStageId);
1019
+ }
1020
+ const rows = this.db
1021
+ .prepare(`SELECT run_id, generation_id, replay_id, fork_parent_stage_id, generation_number,
1022
+ clone_stage_ids_json, status, created_at, updated_at
1023
+ FROM fork_generations
1024
+ WHERE ${clauses.join(" AND ")}
1025
+ ORDER BY created_at ASC`)
1026
+ .all(...params);
1027
+ return rows.map(forkGenerationFromRow);
1028
+ }
1029
+ async updateForkGeneration(runId, generationId, patch) {
1030
+ await this.ready();
1031
+ const sets = ["updated_at = @updated_at"];
1032
+ const params = {
1033
+ run_id: runId,
1034
+ generation_id: generationId,
1035
+ updated_at: new Date().toISOString(),
1036
+ };
1037
+ if (patch.status !== undefined) {
1038
+ sets.push("status = @status");
1039
+ params.status = patch.status;
1040
+ }
1041
+ const result = this.db
1042
+ .prepare(`UPDATE fork_generations SET ${sets.join(", ")}
1043
+ WHERE run_id = @run_id AND generation_id = @generation_id`)
1044
+ .run(params);
1045
+ if (result.changes === 0) {
1046
+ throw new Error(`Fork generation not found: ${runId}/${generationId}`);
1047
+ }
1048
+ }
1049
+ async loadFeedbackLoopHistory(runId) {
1050
+ const loops = await this.listFeedbackLoops(runId);
1051
+ const allForkGens = await this.listForkGenerations(runId);
1052
+ const nullReplayGens = allForkGens.filter((g) => g.replay_id == null);
1053
+ const history = [];
1054
+ for (const loop of loops) {
1055
+ const replays = await this.listFeedbackReplays(runId, loop.loop_id);
1056
+ const replayEntries = [];
1057
+ for (const replay of replays) {
1058
+ const stage_passes = await this.listFeedbackReplayStagePasses(runId, replay.replay_id);
1059
+ const fork_generations = allForkGens.filter((g) => g.replay_id === replay.replay_id);
1060
+ replayEntries.push({ replay, stage_passes, fork_generations });
1061
+ }
1062
+ history.push({
1063
+ loop,
1064
+ replays: replayEntries,
1065
+ fork_generations: nullReplayGens,
1066
+ });
1067
+ }
1068
+ const activeCandidates = loops.filter((l) => l.state === "active" || l.state === "waiting_for_human");
1069
+ let active;
1070
+ if (activeCandidates.length > 0) {
1071
+ active = [...activeCandidates].sort((a, b) => {
1072
+ const byUpdated = b.updated_at.localeCompare(a.updated_at);
1073
+ if (byUpdated !== 0)
1074
+ return byUpdated;
1075
+ return b.created_at.localeCompare(a.created_at);
1076
+ })[0];
1077
+ }
1078
+ return {
1079
+ history,
1080
+ ...(active !== undefined ? { active } : {}),
1081
+ };
464
1082
  }
465
1083
  readPipelineDagSnapshotFromRow(row) {
466
1084
  if (!row.pipeline_dag_json)