@thispointon/kondi-chat 0.1.2

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 (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +556 -0
  3. package/bin/kondi-chat +56 -0
  4. package/bin/kondi-chat.js +72 -0
  5. package/package.json +55 -0
  6. package/scripts/demo.tape +49 -0
  7. package/scripts/postinstall.cjs +103 -0
  8. package/src/audit/analytics.ts +261 -0
  9. package/src/audit/ledger.ts +253 -0
  10. package/src/audit/telemetry.ts +165 -0
  11. package/src/cli/backend.ts +675 -0
  12. package/src/cli/commands.ts +419 -0
  13. package/src/cli/help.ts +182 -0
  14. package/src/cli/submit-helpers.ts +159 -0
  15. package/src/cli/submit.ts +539 -0
  16. package/src/cli/wizard.ts +121 -0
  17. package/src/context/bootstrap.ts +138 -0
  18. package/src/context/budget.ts +100 -0
  19. package/src/context/manager.ts +666 -0
  20. package/src/context/memory.ts +160 -0
  21. package/src/context/preflight.ts +176 -0
  22. package/src/context/project-brain.ts +101 -0
  23. package/src/context/receipts.ts +108 -0
  24. package/src/context/skills.ts +154 -0
  25. package/src/context/symbol-index.ts +240 -0
  26. package/src/council/profiles.ts +137 -0
  27. package/src/council/tool.ts +138 -0
  28. package/src/council-engine/cli/council-artifacts.ts +230 -0
  29. package/src/council-engine/cli/council-config.ts +178 -0
  30. package/src/council-engine/cli/council-session-export.ts +116 -0
  31. package/src/council-engine/cli/kondi.ts +98 -0
  32. package/src/council-engine/cli/llm-caller.ts +229 -0
  33. package/src/council-engine/cli/localStorage-shim.ts +119 -0
  34. package/src/council-engine/cli/node-platform.ts +68 -0
  35. package/src/council-engine/cli/run-council.ts +481 -0
  36. package/src/council-engine/cli/run-pipeline.ts +772 -0
  37. package/src/council-engine/cli/session-export.ts +153 -0
  38. package/src/council-engine/configs/councils/analysis.json +101 -0
  39. package/src/council-engine/configs/councils/code-planning.json +86 -0
  40. package/src/council-engine/configs/councils/coding.json +89 -0
  41. package/src/council-engine/configs/councils/debate.json +97 -0
  42. package/src/council-engine/configs/councils/solo-claude.json +34 -0
  43. package/src/council-engine/configs/councils/solo-gpt.json +34 -0
  44. package/src/council-engine/council/coding-orchestrator.ts +1205 -0
  45. package/src/council-engine/council/context-bootstrap.ts +147 -0
  46. package/src/council-engine/council/context-inspection.ts +42 -0
  47. package/src/council-engine/council/context-store.ts +763 -0
  48. package/src/council-engine/council/deliberation-orchestrator.ts +2762 -0
  49. package/src/council-engine/council/factory.ts +164 -0
  50. package/src/council-engine/council/index.ts +201 -0
  51. package/src/council-engine/council/ledger-store.ts +438 -0
  52. package/src/council-engine/council/prompts.ts +1689 -0
  53. package/src/council-engine/council/storage-cleanup.ts +164 -0
  54. package/src/council-engine/council/store.ts +1110 -0
  55. package/src/council-engine/council/synthesis.ts +291 -0
  56. package/src/council-engine/council/types.ts +845 -0
  57. package/src/council-engine/council/validation.ts +613 -0
  58. package/src/council-engine/pipeline/build-detect.ts +73 -0
  59. package/src/council-engine/pipeline/executor.ts +1048 -0
  60. package/src/council-engine/pipeline/index.ts +9 -0
  61. package/src/council-engine/pipeline/install-detect.ts +84 -0
  62. package/src/council-engine/pipeline/memory-store.ts +182 -0
  63. package/src/council-engine/pipeline/output-parsers.ts +146 -0
  64. package/src/council-engine/pipeline/run-output.ts +149 -0
  65. package/src/council-engine/pipeline/session-import.ts +177 -0
  66. package/src/council-engine/pipeline/store.ts +753 -0
  67. package/src/council-engine/pipeline/test-detect.ts +82 -0
  68. package/src/council-engine/pipeline/types.ts +401 -0
  69. package/src/council-engine/services/deliberationSummary.ts +114 -0
  70. package/src/council-engine/tsconfig.json +16 -0
  71. package/src/council-engine/types/mcp.ts +122 -0
  72. package/src/council-engine/utils/filterTools.ts +73 -0
  73. package/src/engine/apply.ts +238 -0
  74. package/src/engine/checkpoints.ts +237 -0
  75. package/src/engine/consultants.ts +347 -0
  76. package/src/engine/diff.ts +171 -0
  77. package/src/engine/errors.ts +102 -0
  78. package/src/engine/git-tools.ts +246 -0
  79. package/src/engine/hooks.ts +181 -0
  80. package/src/engine/loop-guard.ts +155 -0
  81. package/src/engine/permissions.ts +293 -0
  82. package/src/engine/pipeline.ts +376 -0
  83. package/src/engine/sub-agents.ts +133 -0
  84. package/src/engine/task-card.ts +185 -0
  85. package/src/engine/task-router.ts +256 -0
  86. package/src/engine/task-store.ts +86 -0
  87. package/src/engine/tools.ts +783 -0
  88. package/src/engine/verify.ts +111 -0
  89. package/src/mcp/client.ts +225 -0
  90. package/src/mcp/config.ts +120 -0
  91. package/src/mcp/tool-manager.ts +192 -0
  92. package/src/mcp/types.ts +61 -0
  93. package/src/providers/llm-caller.ts +943 -0
  94. package/src/providers/rate-limiter.ts +238 -0
  95. package/src/router/NOTES.md +28 -0
  96. package/src/router/collector.ts +474 -0
  97. package/src/router/embeddings.ts +286 -0
  98. package/src/router/index.ts +299 -0
  99. package/src/router/intent-router.ts +225 -0
  100. package/src/router/nn-router.ts +205 -0
  101. package/src/router/profiles.ts +309 -0
  102. package/src/router/registry.ts +565 -0
  103. package/src/router/rules.ts +274 -0
  104. package/src/router/train.py +408 -0
  105. package/src/session/store.ts +211 -0
  106. package/src/test-utils/mock-llm.ts +39 -0
  107. package/src/types.ts +322 -0
  108. package/src/web/manager.ts +311 -0
@@ -0,0 +1,753 @@
1
+ /**
2
+ * Pipeline Store: Persistence & State Management
3
+ * localStorage-backed CRUD with subscribe/notify pattern (follows councilStore)
4
+ */
5
+
6
+ import type {
7
+ Pipeline,
8
+ PipelineStage,
9
+ PipelineStep,
10
+ PipelineStepStatus,
11
+ PipelineStatus,
12
+ StepConfig,
13
+ StepArtifact,
14
+ LlmStepConfig,
15
+ } from './types';
16
+ import { migrateLlmConfig } from './types';
17
+ import { councilDataStore } from '../council/storage-cleanup';
18
+ import { deleteCouncilWithData } from '../council/store';
19
+
20
+ const STORAGE_KEY = 'mcp-pipelines';
21
+
22
+ interface StorageData {
23
+ version: number;
24
+ pipelines: Pipeline[];
25
+ lastUpdated: string;
26
+ }
27
+
28
+ // ============================================================================
29
+ // Storage Helpers
30
+ // ============================================================================
31
+
32
+ function migrateV1toV2(data: StorageData): StorageData {
33
+ if (data.version >= 2) return data;
34
+
35
+ console.log('[PipelineStore] Migrating v1 \u2192 v2: council\u2192planning, execution stays, gate stays');
36
+ for (const pipeline of data.pipelines) {
37
+ for (const stage of pipeline.stages) {
38
+ for (const step of stage.steps) {
39
+ if ((step.config as { type: string }).type === 'council') {
40
+ (step.config as { type: string }).type = 'planning';
41
+ }
42
+ // 'execution' and 'gate' stay unchanged
43
+ }
44
+ }
45
+ }
46
+ data.version = 2;
47
+ return data;
48
+ }
49
+
50
+ /**
51
+ * v2 → v3: Convert legacy LlmStepConfig (flat model/provider/systemPrompt)
52
+ * to CouncilStepConfig (with councilSetup). All non-gate step types are now councils.
53
+ */
54
+ function migrateV2toV3(data: StorageData): StorageData {
55
+ if (data.version >= 3) return data;
56
+
57
+ let migrated = 0;
58
+ for (const pipeline of data.pipelines) {
59
+ for (const stage of pipeline.stages) {
60
+ for (const step of stage.steps) {
61
+ const config = step.config as any;
62
+ // Detect legacy LlmStepConfig: has type decisioning/execution but no councilSetup
63
+ if ((config.type === 'decisioning' || config.type === 'execution') && !config.councilSetup) {
64
+ step.config = migrateLlmConfig(config as LlmStepConfig);
65
+ migrated++;
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ if (migrated > 0) {
72
+ console.log(`[PipelineStore] Migrating v2 → v3: converted ${migrated} LLM step(s) to council format`);
73
+ }
74
+ data.version = 3;
75
+ return data;
76
+ }
77
+
78
+ /**
79
+ * v3 → v4: Rename step types to broader names.
80
+ * planning→council, decisioning→analysis, execution→agent, review-docs→review, enrichment→enrich.
81
+ */
82
+ function migrateV3toV4(data: StorageData): StorageData {
83
+ if (data.version >= 4) return data;
84
+
85
+ const typeMap: Record<string, string> = {
86
+ planning: 'council',
87
+ decisioning: 'analysis',
88
+ execution: 'agent',
89
+ 'review-docs': 'review',
90
+ enrichment: 'enrich',
91
+ };
92
+
93
+ let migrated = 0;
94
+ for (const pipeline of data.pipelines) {
95
+ for (const stage of pipeline.stages) {
96
+ for (const step of stage.steps) {
97
+ const config = step.config as { type: string };
98
+ const newType = typeMap[config.type];
99
+ if (newType) {
100
+ config.type = newType;
101
+ migrated++;
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ if (migrated > 0) {
108
+ console.log(`[PipelineStore] Migrating v3 → v4: renamed ${migrated} step type(s)`);
109
+ }
110
+ data.version = 4;
111
+ return data;
112
+ }
113
+
114
+ /**
115
+ * v4 → v5: Rename 'council' → 'code_planning'.
116
+ * The old 'council' type was specifically for code planning (PLAN_TOOLS, planning prompts).
117
+ * Now 'council' is a new open-ended deliberation type, and old council steps become 'code_planning'.
118
+ */
119
+ function migrateV4toV5(data: StorageData): StorageData {
120
+ if (data.version >= 5) return data;
121
+
122
+ let migrated = 0;
123
+ for (const pipeline of data.pipelines) {
124
+ for (const stage of pipeline.stages) {
125
+ for (const step of stage.steps) {
126
+ const config = step.config as { type: string };
127
+ if (config.type === 'council') {
128
+ config.type = 'code_planning';
129
+ migrated++;
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ if (migrated > 0) {
136
+ console.log(`[PipelineStore] Migrating v4 → v5: renamed ${migrated} 'council' step(s) to 'code_planning'`);
137
+ }
138
+ data.version = 5;
139
+ return data;
140
+ }
141
+
142
+ function loadFromStorage(): StorageData {
143
+ try {
144
+ const raw = councilDataStore.getItem(STORAGE_KEY);
145
+ if (!raw) {
146
+ return { version: 5, pipelines: [], lastUpdated: new Date().toISOString() };
147
+ }
148
+ let data = JSON.parse(raw) as StorageData;
149
+ data = migrateV1toV2(data);
150
+ data = migrateV2toV3(data);
151
+ data = migrateV3toV4(data);
152
+ data = migrateV4toV5(data);
153
+ return data;
154
+ } catch (error) {
155
+ console.error('[PipelineStore] Failed to load from storage:', error);
156
+ return { version: 5, pipelines: [], lastUpdated: new Date().toISOString() };
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Reset any pipelines/steps that were left in a transient state (running, waiting)
162
+ * from a previous session. Called once on startup.
163
+ */
164
+ function resetStaleExecutionStates(): void {
165
+ const data = loadFromStorage();
166
+ let dirty = false;
167
+
168
+ for (const pipeline of data.pipelines) {
169
+ if (pipeline.status === 'running' || pipeline.status === 'paused') {
170
+ pipeline.status = 'failed';
171
+ dirty = true;
172
+ }
173
+
174
+ for (const stage of pipeline.stages) {
175
+ for (const step of stage.steps) {
176
+ if (step.status === 'running' || step.status === 'waiting_approval') {
177
+ step.status = 'failed';
178
+ step.error = 'Interrupted: application was restarted';
179
+ step.completedAt = new Date().toISOString();
180
+ dirty = true;
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ if (dirty) {
187
+ saveToStorage(data);
188
+ console.log('[PipelineStore] Reset stale running/waiting states from previous session');
189
+ }
190
+ }
191
+
192
+ // Run once on module load
193
+ resetStaleExecutionStates();
194
+
195
+ function saveToStorage(data: StorageData): void {
196
+ data.lastUpdated = new Date().toISOString();
197
+ // Use persistent save — pipeline configs MUST survive app restarts
198
+ councilDataStore.setItemPersistent(STORAGE_KEY, JSON.stringify(data));
199
+ }
200
+
201
+ // ============================================================================
202
+ // Pipeline CRUD
203
+ // ============================================================================
204
+
205
+ export function getAllPipelines(): Pipeline[] {
206
+ const data = loadFromStorage();
207
+ return data.pipelines.sort(
208
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
209
+ );
210
+ }
211
+
212
+ export function getPipeline(id: string): Pipeline | null {
213
+ const data = loadFromStorage();
214
+ return data.pipelines.find((p) => p.id === id) || null;
215
+ }
216
+
217
+ export function createPipeline(params: {
218
+ name: string;
219
+ description?: string;
220
+ initialInput?: string;
221
+ settings?: Partial<Pipeline['settings']>;
222
+ }): Pipeline {
223
+ const now = new Date().toISOString();
224
+
225
+ const pipeline: Pipeline = {
226
+ id: crypto.randomUUID(),
227
+ name: params.name,
228
+ description: params.description,
229
+ initialInput: params.initialInput || '',
230
+ stages: [],
231
+ settings: {
232
+ workingDirectory: params.settings?.workingDirectory,
233
+ failurePolicy: params.settings?.failurePolicy || 'stop',
234
+ directoryConstrained: params.settings?.directoryConstrained ?? true,
235
+ },
236
+ status: 'draft',
237
+ currentStageIndex: 0,
238
+ createdAt: now,
239
+ updatedAt: now,
240
+ };
241
+
242
+ const data = loadFromStorage();
243
+ data.pipelines.push(pipeline);
244
+ saveToStorage(data);
245
+
246
+ console.log('[PipelineStore] Created pipeline:', pipeline.id, pipeline.name);
247
+ return pipeline;
248
+ }
249
+
250
+ export function updatePipeline(
251
+ id: string,
252
+ updates: Partial<Omit<Pipeline, 'id' | 'createdAt'>>
253
+ ): Pipeline | null {
254
+ const data = loadFromStorage();
255
+ const index = data.pipelines.findIndex((p) => p.id === id);
256
+
257
+ if (index === -1) {
258
+ console.warn('[PipelineStore] Pipeline not found:', id);
259
+ return null;
260
+ }
261
+
262
+ const updated: Pipeline = {
263
+ ...data.pipelines[index],
264
+ ...updates,
265
+ updatedAt: new Date().toISOString(),
266
+ };
267
+
268
+ data.pipelines[index] = updated;
269
+ saveToStorage(data);
270
+
271
+ return updated;
272
+ }
273
+
274
+ export function deletePipeline(id: string): boolean {
275
+ const data = loadFromStorage();
276
+ const index = data.pipelines.findIndex((p) => p.id === id);
277
+
278
+ if (index === -1) return false;
279
+
280
+ data.pipelines.splice(index, 1);
281
+ saveToStorage(data);
282
+
283
+ console.log('[PipelineStore] Deleted pipeline:', id);
284
+ return true;
285
+ }
286
+
287
+ export function duplicatePipeline(id: string, newName?: string): Pipeline | null {
288
+ const original = getPipeline(id);
289
+ if (!original) return null;
290
+
291
+ const now = new Date().toISOString();
292
+
293
+ // Deep clone stages with new IDs
294
+ const clonedStages: PipelineStage[] = original.stages.map((stage) => ({
295
+ id: crypto.randomUUID(),
296
+ name: stage.name,
297
+ steps: stage.steps.map((step) => ({
298
+ ...step,
299
+ id: crypto.randomUUID(),
300
+ status: 'pending' as const,
301
+ artifact: undefined,
302
+ error: undefined,
303
+ startedAt: undefined,
304
+ completedAt: undefined,
305
+ })),
306
+ }));
307
+
308
+ const duplicate: Pipeline = {
309
+ ...original,
310
+ id: crypto.randomUUID(),
311
+ name: newName || `${original.name} (Copy)`,
312
+ stages: clonedStages,
313
+ status: 'draft',
314
+ currentStageIndex: 0,
315
+ createdAt: now,
316
+ updatedAt: now,
317
+ };
318
+
319
+ const data = loadFromStorage();
320
+ data.pipelines.push(duplicate);
321
+ saveToStorage(data);
322
+
323
+ return duplicate;
324
+ }
325
+
326
+ // ============================================================================
327
+ // Stage Operations
328
+ // ============================================================================
329
+
330
+ export function addStage(
331
+ pipelineId: string,
332
+ name?: string,
333
+ atIndex?: number
334
+ ): Pipeline | null {
335
+ const pipeline = getPipeline(pipelineId);
336
+ if (!pipeline) return null;
337
+
338
+ const stage: PipelineStage = {
339
+ id: crypto.randomUUID(),
340
+ name: name || `Stage ${pipeline.stages.length + 1}`,
341
+ steps: [],
342
+ };
343
+
344
+ const stages = [...pipeline.stages];
345
+ if (atIndex !== undefined && atIndex >= 0 && atIndex <= stages.length) {
346
+ stages.splice(atIndex, 0, stage);
347
+ } else {
348
+ stages.push(stage);
349
+ }
350
+
351
+ return updatePipeline(pipelineId, { stages });
352
+ }
353
+
354
+ export function removeStage(pipelineId: string, stageId: string): Pipeline | null {
355
+ const pipeline = getPipeline(pipelineId);
356
+ if (!pipeline) return null;
357
+
358
+ return updatePipeline(pipelineId, {
359
+ stages: pipeline.stages.filter((s) => s.id !== stageId),
360
+ });
361
+ }
362
+
363
+ export function updateStage(
364
+ pipelineId: string,
365
+ stageId: string,
366
+ updates: Partial<Omit<PipelineStage, 'id'>>
367
+ ): Pipeline | null {
368
+ const pipeline = getPipeline(pipelineId);
369
+ if (!pipeline) return null;
370
+
371
+ const stages = pipeline.stages.map((s) =>
372
+ s.id === stageId ? { ...s, ...updates } : s
373
+ );
374
+
375
+ return updatePipeline(pipelineId, { stages });
376
+ }
377
+
378
+ export function reorderStages(
379
+ pipelineId: string,
380
+ stageIds: string[]
381
+ ): Pipeline | null {
382
+ const pipeline = getPipeline(pipelineId);
383
+ if (!pipeline) return null;
384
+
385
+ const stageMap = new Map(pipeline.stages.map((s) => [s.id, s]));
386
+ const reordered = stageIds
387
+ .map((id) => stageMap.get(id))
388
+ .filter((s): s is PipelineStage => s !== undefined);
389
+
390
+ if (reordered.length !== pipeline.stages.length) return null;
391
+
392
+ return updatePipeline(pipelineId, { stages: reordered });
393
+ }
394
+
395
+ // ============================================================================
396
+ // Step Operations
397
+ // ============================================================================
398
+
399
+ export function addStep(
400
+ pipelineId: string,
401
+ stageId: string,
402
+ config: StepConfig,
403
+ name?: string
404
+ ): Pipeline | null {
405
+ const pipeline = getPipeline(pipelineId);
406
+ if (!pipeline) return null;
407
+
408
+ const step: PipelineStep = {
409
+ id: crypto.randomUUID(),
410
+ name: name || `Step ${pipeline.stages.find((s) => s.id === stageId)?.steps.length || 0 + 1}`,
411
+ config,
412
+ status: 'pending',
413
+ };
414
+
415
+ const stages = pipeline.stages.map((s) =>
416
+ s.id === stageId ? { ...s, steps: [...s.steps, step] } : s
417
+ );
418
+
419
+ return updatePipeline(pipelineId, { stages });
420
+ }
421
+
422
+ export function removeStep(
423
+ pipelineId: string,
424
+ stageId: string,
425
+ stepId: string
426
+ ): Pipeline | null {
427
+ const pipeline = getPipeline(pipelineId);
428
+ if (!pipeline) return null;
429
+
430
+ const stages = pipeline.stages.map((s) =>
431
+ s.id === stageId
432
+ ? { ...s, steps: s.steps.filter((st) => st.id !== stepId) }
433
+ : s
434
+ );
435
+
436
+ return updatePipeline(pipelineId, { stages });
437
+ }
438
+
439
+ export function updateStep(
440
+ pipelineId: string,
441
+ stepId: string,
442
+ updates: Partial<Omit<PipelineStep, 'id'>>
443
+ ): Pipeline | null {
444
+ const pipeline = getPipeline(pipelineId);
445
+ if (!pipeline) return null;
446
+
447
+ const stages = pipeline.stages.map((stage) => ({
448
+ ...stage,
449
+ steps: stage.steps.map((step) =>
450
+ step.id === stepId ? { ...step, ...updates } : step
451
+ ),
452
+ }));
453
+
454
+ return updatePipeline(pipelineId, { stages });
455
+ }
456
+
457
+ export function updateStepConfig(
458
+ pipelineId: string,
459
+ stepId: string,
460
+ config: StepConfig
461
+ ): Pipeline | null {
462
+ return updateStep(pipelineId, stepId, { config });
463
+ }
464
+
465
+ // ============================================================================
466
+ // Execution State Operations
467
+ // ============================================================================
468
+
469
+ export function setStepStatus(
470
+ pipelineId: string,
471
+ stepId: string,
472
+ status: PipelineStepStatus,
473
+ error?: string
474
+ ): Pipeline | null {
475
+ const updates: Partial<PipelineStep> = { status };
476
+ if (status === 'running') {
477
+ updates.startedAt = new Date().toISOString();
478
+ // Clear previous error and completion when starting fresh
479
+ updates.error = undefined;
480
+ updates.completedAt = undefined;
481
+ }
482
+ if (status === 'completed' || status === 'failed') {
483
+ updates.completedAt = new Date().toISOString();
484
+ }
485
+ if (error) {
486
+ updates.error = error;
487
+ }
488
+ return updateStep(pipelineId, stepId, updates);
489
+ }
490
+
491
+ export function setStepArtifact(
492
+ pipelineId: string,
493
+ stepId: string,
494
+ artifact: StepArtifact
495
+ ): Pipeline | null {
496
+ return updateStep(pipelineId, stepId, { artifact });
497
+ }
498
+
499
+ export function setPipelineStatus(
500
+ pipelineId: string,
501
+ status: PipelineStatus
502
+ ): Pipeline | null {
503
+ return updatePipeline(pipelineId, { status });
504
+ }
505
+
506
+ export function advanceStage(pipelineId: string): Pipeline | null {
507
+ const pipeline = getPipeline(pipelineId);
508
+ if (!pipeline) return null;
509
+
510
+ return updatePipeline(pipelineId, {
511
+ currentStageIndex: pipeline.currentStageIndex + 1,
512
+ });
513
+ }
514
+
515
+ /**
516
+ * Delete council deliberation data (ledger, context, decisions) for steps
517
+ * that are about to be reset. Without this, the UI shows stale entries
518
+ * from the previous run mixed with the new one.
519
+ */
520
+ function purgeCouncilsForSteps(steps: PipelineStep[]): void {
521
+ for (const step of steps) {
522
+ const councilId = step.artifact?.metadata?.councilId;
523
+ if (councilId) {
524
+ try {
525
+ deleteCouncilWithData(councilId);
526
+ } catch (err) {
527
+ console.warn('[PipelineStore] Failed to delete council data:', councilId, err);
528
+ }
529
+ }
530
+ }
531
+ }
532
+
533
+ export function resetExecution(pipelineId: string): Pipeline | null {
534
+ const pipeline = getPipeline(pipelineId);
535
+ if (!pipeline) return null;
536
+
537
+ // Delete all council deliberation data before resetting steps
538
+ const allSteps = pipeline.stages.flatMap((s) => s.steps);
539
+ purgeCouncilsForSteps(allSteps);
540
+
541
+ const stages = pipeline.stages.map((stage) => ({
542
+ ...stage,
543
+ steps: stage.steps.map((step) => ({
544
+ ...step,
545
+ status: 'pending' as const,
546
+ artifact: undefined,
547
+ error: undefined,
548
+ startedAt: undefined,
549
+ completedAt: undefined,
550
+ })),
551
+ }));
552
+
553
+ return updatePipeline(pipelineId, {
554
+ stages,
555
+ status: 'draft',
556
+ currentStageIndex: 0,
557
+ });
558
+ }
559
+
560
+ /**
561
+ * Reset a specific step and all subsequent steps to pending.
562
+ * Sets currentStageIndex to the target step's stage so the executor
563
+ * resumes from the right point. Earlier completed steps are preserved.
564
+ */
565
+ export function resetStepAndAfter(pipelineId: string, stepId: string): Pipeline | null {
566
+ const pipeline = getPipeline(pipelineId);
567
+ if (!pipeline) return null;
568
+
569
+ // Find the step's stage index and step index
570
+ let targetStageIndex = -1;
571
+ let targetStepIndex = -1;
572
+ for (let si = 0; si < pipeline.stages.length; si++) {
573
+ for (let sti = 0; sti < pipeline.stages[si].steps.length; sti++) {
574
+ if (pipeline.stages[si].steps[sti].id === stepId) {
575
+ targetStageIndex = si;
576
+ targetStepIndex = sti;
577
+ break;
578
+ }
579
+ }
580
+ if (targetStageIndex >= 0) break;
581
+ }
582
+ if (targetStageIndex < 0) return null;
583
+
584
+ // Delete council deliberation data for steps being reset
585
+ const stepsToReset: PipelineStep[] = [];
586
+ for (let si = targetStageIndex; si < pipeline.stages.length; si++) {
587
+ for (let sti = 0; sti < pipeline.stages[si].steps.length; sti++) {
588
+ if (si > targetStageIndex || sti >= targetStepIndex) {
589
+ stepsToReset.push(pipeline.stages[si].steps[sti]);
590
+ }
591
+ }
592
+ }
593
+ purgeCouncilsForSteps(stepsToReset);
594
+
595
+ const stages = pipeline.stages.map((stage, si) => {
596
+ if (si < targetStageIndex) return stage; // earlier stages untouched
597
+ return {
598
+ ...stage,
599
+ steps: stage.steps.map((step, sti) => {
600
+ // In the target stage: reset this step and all after it
601
+ // In later stages: reset everything
602
+ if (si > targetStageIndex || sti >= targetStepIndex) {
603
+ return {
604
+ ...step,
605
+ status: 'pending' as const,
606
+ artifact: undefined,
607
+ error: undefined,
608
+ startedAt: undefined,
609
+ completedAt: undefined,
610
+ };
611
+ }
612
+ return step;
613
+ }),
614
+ };
615
+ });
616
+
617
+ return updatePipeline(pipelineId, {
618
+ stages,
619
+ status: 'ready',
620
+ currentStageIndex: targetStageIndex,
621
+ });
622
+ }
623
+
624
+ // ============================================================================
625
+ // Store Class (for React integration)
626
+ // ============================================================================
627
+
628
+ export class PipelineStore {
629
+ private listeners: Set<() => void> = new Set();
630
+
631
+ subscribe(listener: () => void): () => void {
632
+ this.listeners.add(listener);
633
+ return () => this.listeners.delete(listener);
634
+ }
635
+
636
+ private notify(): void {
637
+ this.listeners.forEach((listener) => listener());
638
+ }
639
+
640
+ getAll = getAllPipelines;
641
+ get = getPipeline;
642
+
643
+ create(params: Parameters<typeof createPipeline>[0]): Pipeline {
644
+ const pipeline = createPipeline(params);
645
+ this.notify();
646
+ return pipeline;
647
+ }
648
+
649
+ update(id: string, updates: Parameters<typeof updatePipeline>[1]): Pipeline | null {
650
+ const pipeline = updatePipeline(id, updates);
651
+ if (pipeline) this.notify();
652
+ return pipeline;
653
+ }
654
+
655
+ delete(id: string): boolean {
656
+ const success = deletePipeline(id);
657
+ if (success) this.notify();
658
+ return success;
659
+ }
660
+
661
+ duplicate(id: string, newName?: string): Pipeline | null {
662
+ const pipeline = duplicatePipeline(id, newName);
663
+ if (pipeline) this.notify();
664
+ return pipeline;
665
+ }
666
+
667
+ addStage(pipelineId: string, name?: string, atIndex?: number): Pipeline | null {
668
+ const pipeline = addStage(pipelineId, name, atIndex);
669
+ if (pipeline) this.notify();
670
+ return pipeline;
671
+ }
672
+
673
+ removeStage(pipelineId: string, stageId: string): Pipeline | null {
674
+ const pipeline = removeStage(pipelineId, stageId);
675
+ if (pipeline) this.notify();
676
+ return pipeline;
677
+ }
678
+
679
+ updateStage(pipelineId: string, stageId: string, updates: Partial<Omit<PipelineStage, 'id'>>): Pipeline | null {
680
+ const pipeline = updateStage(pipelineId, stageId, updates);
681
+ if (pipeline) this.notify();
682
+ return pipeline;
683
+ }
684
+
685
+ reorderStages(pipelineId: string, stageIds: string[]): Pipeline | null {
686
+ const pipeline = reorderStages(pipelineId, stageIds);
687
+ if (pipeline) this.notify();
688
+ return pipeline;
689
+ }
690
+
691
+ addStep(pipelineId: string, stageId: string, config: StepConfig, name?: string): Pipeline | null {
692
+ const pipeline = addStep(pipelineId, stageId, config, name);
693
+ if (pipeline) this.notify();
694
+ return pipeline;
695
+ }
696
+
697
+ removeStep(pipelineId: string, stageId: string, stepId: string): Pipeline | null {
698
+ const pipeline = removeStep(pipelineId, stageId, stepId);
699
+ if (pipeline) this.notify();
700
+ return pipeline;
701
+ }
702
+
703
+ updateStep(pipelineId: string, stepId: string, updates: Partial<Omit<PipelineStep, 'id'>>): Pipeline | null {
704
+ const pipeline = updateStep(pipelineId, stepId, updates);
705
+ if (pipeline) this.notify();
706
+ return pipeline;
707
+ }
708
+
709
+ updateStepConfig(pipelineId: string, stepId: string, config: StepConfig): Pipeline | null {
710
+ const pipeline = updateStepConfig(pipelineId, stepId, config);
711
+ if (pipeline) this.notify();
712
+ return pipeline;
713
+ }
714
+
715
+ setStepStatus(pipelineId: string, stepId: string, status: PipelineStepStatus, error?: string): Pipeline | null {
716
+ const pipeline = setStepStatus(pipelineId, stepId, status, error);
717
+ if (pipeline) this.notify();
718
+ return pipeline;
719
+ }
720
+
721
+ setStepArtifact(pipelineId: string, stepId: string, artifact: StepArtifact): Pipeline | null {
722
+ const pipeline = setStepArtifact(pipelineId, stepId, artifact);
723
+ if (pipeline) this.notify();
724
+ return pipeline;
725
+ }
726
+
727
+ setPipelineStatus(pipelineId: string, status: PipelineStatus): Pipeline | null {
728
+ const pipeline = setPipelineStatus(pipelineId, status);
729
+ if (pipeline) this.notify();
730
+ return pipeline;
731
+ }
732
+
733
+ advanceStage(pipelineId: string): Pipeline | null {
734
+ const pipeline = advanceStage(pipelineId);
735
+ if (pipeline) this.notify();
736
+ return pipeline;
737
+ }
738
+
739
+ resetExecution(pipelineId: string): Pipeline | null {
740
+ const pipeline = resetExecution(pipelineId);
741
+ if (pipeline) this.notify();
742
+ return pipeline;
743
+ }
744
+
745
+ resetStepAndAfter(pipelineId: string, stepId: string): Pipeline | null {
746
+ const pipeline = resetStepAndAfter(pipelineId, stepId);
747
+ if (pipeline) this.notify();
748
+ return pipeline;
749
+ }
750
+ }
751
+
752
+ // Singleton instance
753
+ export const pipelineStore = new PipelineStore();