forge-server 0.1.0 → 0.1.1

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 (119) hide show
  1. package/bin/setup-forge.sh +1 -1
  2. package/bin/setup.js +99 -0
  3. package/dist/cli.js +37 -37
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.js +4 -4
  6. package/dist/index.js.map +1 -1
  7. package/dist/storage/schema.js +113 -113
  8. package/dist/storage/schema.js.map +1 -1
  9. package/dist/storage/sqlite.js +1 -1
  10. package/dist/storage/sqlite.js.map +1 -1
  11. package/dist/util/logger.d.ts +1 -1
  12. package/dist/util/logger.js +1 -1
  13. package/dist/util/types.js +1 -1
  14. package/dist/util/types.js.map +1 -1
  15. package/package.json +8 -2
  16. package/plugin/.mcp.json +1 -1
  17. package/.claude/hooks/worktree-create.sh +0 -64
  18. package/.claude/hooks/worktree-remove.sh +0 -57
  19. package/.claude/settings.local.json +0 -29
  20. package/.forge/knowledge/conventions.yaml +0 -1
  21. package/.forge/knowledge/decisions.yaml +0 -1
  22. package/.forge/knowledge/gotchas.yaml +0 -1
  23. package/.forge/knowledge/patterns.yaml +0 -1
  24. package/.forge/manifest.yaml +0 -6
  25. package/CLAUDE.md +0 -144
  26. package/docker-compose.yml +0 -20
  27. package/docs/plans/2026-02-27-swarm-coordination/architecture.md +0 -203
  28. package/docs/plans/2026-02-27-swarm-coordination/vision.md +0 -57
  29. package/docs/plans/completed/2026-02-26-forge-plugin-bundling/architecture.md +0 -1
  30. package/docs/plans/completed/2026-02-26-forge-plugin-bundling/vision.md +0 -300
  31. package/docs/plans/completed/2026-02-27-forge-swarm-learning/architecture.md +0 -480
  32. package/docs/plans/completed/2026-02-27-forge-swarm-learning/verification-checklist.md +0 -462
  33. package/docs/plans/completed/2026-02-27-git-history-atlassian/git-jira-plan.md +0 -181
  34. package/src/cli.ts +0 -655
  35. package/src/context/.gitkeep +0 -0
  36. package/src/context/codebase.ts +0 -393
  37. package/src/context/injector.ts +0 -797
  38. package/src/context/memory.ts +0 -187
  39. package/src/context/session-index.ts +0 -327
  40. package/src/context/session.ts +0 -152
  41. package/src/index.ts +0 -47
  42. package/src/ingestion/.gitkeep +0 -0
  43. package/src/ingestion/chunker.ts +0 -277
  44. package/src/ingestion/embedder.ts +0 -167
  45. package/src/ingestion/git-analyzer.ts +0 -545
  46. package/src/ingestion/indexer.ts +0 -984
  47. package/src/ingestion/markdown-chunker.ts +0 -337
  48. package/src/ingestion/markdown-knowledge.ts +0 -175
  49. package/src/ingestion/parser.ts +0 -475
  50. package/src/ingestion/watcher.ts +0 -182
  51. package/src/knowledge/.gitkeep +0 -0
  52. package/src/knowledge/hydrator.ts +0 -246
  53. package/src/knowledge/registry.ts +0 -463
  54. package/src/knowledge/search.ts +0 -565
  55. package/src/knowledge/store.ts +0 -262
  56. package/src/learning/.gitkeep +0 -0
  57. package/src/learning/confidence.ts +0 -193
  58. package/src/learning/patterns.ts +0 -360
  59. package/src/learning/trajectory.ts +0 -268
  60. package/src/memory/.gitkeep +0 -0
  61. package/src/memory/memory-compat.ts +0 -233
  62. package/src/memory/observation-store.ts +0 -224
  63. package/src/memory/session-tracker.ts +0 -332
  64. package/src/pipeline/.gitkeep +0 -0
  65. package/src/pipeline/engine.ts +0 -1139
  66. package/src/pipeline/events.ts +0 -253
  67. package/src/pipeline/parallel.ts +0 -394
  68. package/src/pipeline/state-machine.ts +0 -199
  69. package/src/query/.gitkeep +0 -0
  70. package/src/query/graph-queries.ts +0 -262
  71. package/src/query/hybrid-search.ts +0 -337
  72. package/src/query/intent-detector.ts +0 -131
  73. package/src/query/ranking.ts +0 -161
  74. package/src/server.ts +0 -352
  75. package/src/storage/.gitkeep +0 -0
  76. package/src/storage/falkordb-store.ts +0 -388
  77. package/src/storage/file-cache.ts +0 -141
  78. package/src/storage/interfaces.ts +0 -201
  79. package/src/storage/qdrant-store.ts +0 -557
  80. package/src/storage/schema.ts +0 -139
  81. package/src/storage/sqlite.ts +0 -168
  82. package/src/tools/.gitkeep +0 -0
  83. package/src/tools/collaboration-tools.ts +0 -208
  84. package/src/tools/context-tools.ts +0 -493
  85. package/src/tools/graph-tools.ts +0 -295
  86. package/src/tools/ingestion-tools.ts +0 -122
  87. package/src/tools/learning-tools.ts +0 -181
  88. package/src/tools/memory-tools.ts +0 -234
  89. package/src/tools/phase-tools.ts +0 -1452
  90. package/src/tools/pipeline-tools.ts +0 -188
  91. package/src/tools/registration-tools.ts +0 -450
  92. package/src/util/.gitkeep +0 -0
  93. package/src/util/circuit-breaker.ts +0 -193
  94. package/src/util/config.ts +0 -177
  95. package/src/util/logger.ts +0 -53
  96. package/src/util/token-counter.ts +0 -52
  97. package/src/util/types.ts +0 -710
  98. package/tests/context/.gitkeep +0 -0
  99. package/tests/integration/.gitkeep +0 -0
  100. package/tests/knowledge/.gitkeep +0 -0
  101. package/tests/learning/.gitkeep +0 -0
  102. package/tests/pipeline/.gitkeep +0 -0
  103. package/tests/tools/.gitkeep +0 -0
  104. package/tsconfig.json +0 -21
  105. package/vitest.config.ts +0 -10
  106. package/vscode-extension/.vscodeignore +0 -7
  107. package/vscode-extension/README.md +0 -43
  108. package/vscode-extension/out/edge-collector.js +0 -274
  109. package/vscode-extension/out/edge-collector.js.map +0 -1
  110. package/vscode-extension/out/extension.js +0 -264
  111. package/vscode-extension/out/extension.js.map +0 -1
  112. package/vscode-extension/out/forge-client.js +0 -318
  113. package/vscode-extension/out/forge-client.js.map +0 -1
  114. package/vscode-extension/package-lock.json +0 -59
  115. package/vscode-extension/package.json +0 -71
  116. package/vscode-extension/src/edge-collector.ts +0 -320
  117. package/vscode-extension/src/extension.ts +0 -269
  118. package/vscode-extension/src/forge-client.ts +0 -364
  119. package/vscode-extension/tsconfig.json +0 -19
@@ -1,360 +0,0 @@
1
- // PatternExtractor — B11
2
- //
3
- // Extracts learnable knowledge items from completed project trajectories.
4
- // Called by the knowledge_collection phase tool handler after a project
5
- // reaches the inspection->completed transition.
6
- //
7
- // Two operations:
8
- //
9
- // extractFromProject(projectId)
10
- // Reads all trajectory steps for the project, looks for high-quality
11
- // steps (qualityScore >= 0.7) and steps with "gotcha"-like language,
12
- // and promotes the best observations to KnowledgeItem records.
13
- // Writes to both the YAML store (durable) and Qdrant (searchable).
14
- //
15
- // promoteToKnowledge(observation, category, repoId, ...)
16
- // One-shot promotion: takes a raw observation string and creates a
17
- // full KnowledgeItem with embedding, persisting to YAML and Qdrant.
18
- // Used by the knowledge-keeper agent for manual promotion.
19
-
20
- import { randomUUID } from 'node:crypto';
21
- import type { PipelineDB } from '../storage/sqlite.js';
22
- import type { KnowledgeYamlStore } from '../knowledge/store.js';
23
- import type { KnowledgeSearch } from '../knowledge/search.js';
24
- import type {
25
- KnowledgeItem,
26
- KnowledgeCategory,
27
- Phase,
28
- PipelinePhase,
29
- TrajectoryStepRow,
30
- TrajectoryRow,
31
- } from '../util/types.js';
32
- import { embedText } from '../ingestion/embedder.js';
33
- import { logger } from '../util/logger.js';
34
-
35
- // ---------------------------------------------------------------------------
36
- // Constants
37
- // ---------------------------------------------------------------------------
38
-
39
- /** Minimum quality score for a step to be considered a candidate. */
40
- const QUALITY_THRESHOLD = 0.7;
41
-
42
- /** Words that strongly suggest a step contains a gotcha / pain point. */
43
- const GOTCHA_KEYWORDS = [
44
- 'gotcha',
45
- 'bug',
46
- 'fix',
47
- 'broken',
48
- 'failed',
49
- 'error',
50
- 'issue',
51
- 'problem',
52
- 'mistake',
53
- 'avoid',
54
- 'warning',
55
- 'careful',
56
- 'unexpected',
57
- 'pitfall',
58
- 'caveat',
59
- ];
60
-
61
- /** Words that suggest a step contains a positive pattern. */
62
- const PATTERN_KEYWORDS = [
63
- 'pattern',
64
- 'approach',
65
- 'works',
66
- 'solution',
67
- 'discovered',
68
- 'learned',
69
- 'efficient',
70
- 'best',
71
- 'prefer',
72
- 'recommend',
73
- ];
74
-
75
- // ---------------------------------------------------------------------------
76
- // PatternExtractor
77
- // ---------------------------------------------------------------------------
78
-
79
- export class PatternExtractor {
80
- constructor(
81
- private readonly db: PipelineDB,
82
- private readonly knowledgeStore: KnowledgeYamlStore,
83
- private readonly knowledgeSearch: KnowledgeSearch,
84
- private readonly embedder: typeof embedText,
85
- ) {}
86
-
87
- /**
88
- * Extract knowledge from all trajectory steps of a completed project.
89
- *
90
- * Algorithm:
91
- * 1. Load all trajectories + their steps for the project.
92
- * 2. Collect steps that are either:
93
- * a. High quality (qualityScore >= QUALITY_THRESHOLD), or
94
- * b. Contain gotcha-like language in action/result text.
95
- * 3. For each candidate step, determine whether it looks like a gotcha
96
- * or a pattern (keyword matching).
97
- * 4. Deduplicate against existing knowledge (semantic similarity check).
98
- * 5. Promote unique candidates to KnowledgeItem records.
99
- * 6. Return the list of newly created items.
100
- */
101
- async extractFromProject(projectId: string): Promise<KnowledgeItem[]> {
102
- // Load all trajectories for the project
103
- const trajectoryRows = this.db.all<TrajectoryRow>(
104
- `SELECT * FROM trajectories WHERE project_id = ? ORDER BY started_at ASC`,
105
- [projectId],
106
- );
107
-
108
- if (trajectoryRows.length === 0) {
109
- logger.debug('PatternExtractor: no trajectories for project', { projectId });
110
- return [];
111
- }
112
-
113
- // Load all steps for these trajectories in one query
114
- const trajectoryIds = trajectoryRows.map((t) => t.id);
115
- const placeholders = trajectoryIds.map(() => '?').join(', ');
116
- const stepRows = this.db.all<TrajectoryStepRow>(
117
- `SELECT * FROM trajectory_steps WHERE trajectory_id IN (${placeholders}) ORDER BY created_at ASC`,
118
- trajectoryIds,
119
- );
120
-
121
- // Build a map from trajectory_id -> trajectory for phase/agent lookup
122
- const trajectoryMap = new Map<string, TrajectoryRow>(
123
- trajectoryRows.map((t) => [t.id, t]),
124
- );
125
-
126
- // Identify candidate steps
127
- const candidates = stepRows.filter((step) => {
128
- const hasHighQuality =
129
- step.quality_score !== null && step.quality_score >= QUALITY_THRESHOLD;
130
- const text = `${step.action} ${step.result ?? ''}`.toLowerCase();
131
- const hasGotchaLanguage = GOTCHA_KEYWORDS.some((kw) => text.includes(kw));
132
- const hasPatternLanguage = PATTERN_KEYWORDS.some((kw) => text.includes(kw));
133
- return hasHighQuality || hasGotchaLanguage || hasPatternLanguage;
134
- });
135
-
136
- if (candidates.length === 0) {
137
- logger.debug('PatternExtractor: no candidate steps found', { projectId });
138
- return [];
139
- }
140
-
141
- // Look up repo_id from the project
142
- const projectRow = this.db.get<{ repo_id: string }>(
143
- `SELECT repo_id FROM projects WHERE id = ?`,
144
- [projectId],
145
- );
146
- const repoId = projectRow?.repo_id ?? 'unknown';
147
-
148
- // Promote each candidate (with deduplication)
149
- const promoted: KnowledgeItem[] = [];
150
-
151
- for (const step of candidates) {
152
- const traj = trajectoryMap.get(step.trajectory_id);
153
- const observation = buildObservationText(step);
154
- const category = classifyCategory(step);
155
-
156
- try {
157
- // Check for near-duplicate in existing knowledge
158
- const isDuplicate = await this.isDuplicateKnowledge(observation, category);
159
- if (isDuplicate) {
160
- logger.debug('PatternExtractor: skipping duplicate', {
161
- action: step.action.slice(0, 60),
162
- });
163
- continue;
164
- }
165
-
166
- const item = await this.promoteToKnowledge(
167
- observation,
168
- category,
169
- repoId,
170
- traj ? (traj.phase as Phase) : undefined,
171
- traj?.agent,
172
- );
173
- promoted.push(item);
174
- } catch (err) {
175
- // Non-fatal: log and continue to next candidate
176
- logger.warn('PatternExtractor: failed to promote step', {
177
- trajectoryId: step.trajectory_id,
178
- error: String(err),
179
- });
180
- }
181
- }
182
-
183
- logger.info('PatternExtractor: extraction complete', {
184
- projectId,
185
- candidateCount: candidates.length,
186
- promotedCount: promoted.length,
187
- });
188
-
189
- return promoted;
190
- }
191
-
192
- /**
193
- * Promote a raw observation string to a full KnowledgeItem.
194
- *
195
- * Steps:
196
- * 1. Synthesise a title from the first sentence of the observation.
197
- * 2. Build a KnowledgeItem with appropriate defaults.
198
- * 3. Add to the YAML store (the git-tracked source of truth).
199
- * 4. Embed and upsert to Qdrant via hydrateItem() (if available).
200
- * 5. Return the new item.
201
- */
202
- async promoteToKnowledge(
203
- observation: string,
204
- category: KnowledgeCategory,
205
- repoId: string,
206
- phase?: Phase | PipelinePhase,
207
- agent?: string,
208
- ): Promise<KnowledgeItem> {
209
- const now = Date.now();
210
- const prefix = categoryPrefix(category);
211
- const id = `${prefix}-${randomUUID().slice(0, 8)}`;
212
- const title = deriveTitle(observation, category);
213
-
214
- const item: KnowledgeItem = {
215
- id,
216
- title,
217
- content: observation,
218
- stack_tags: [], // Extracted without stack context; caller can enrich
219
- confidence: 0.6, // New items start at moderate confidence
220
- source: 'agent',
221
- // Phase values from both Phase and PipelinePhase unions are valid strings
222
- // in YAML. We cast to satisfy the KnowledgeItem type declaration.
223
- source_phase: (phase ?? null) as PipelinePhase | null,
224
- source_agent: agent ?? null,
225
- created_at: now,
226
- updated_at: now,
227
- };
228
-
229
- // Persist to YAML (source of truth)
230
- this.knowledgeStore.addItem(item);
231
-
232
- // Embed and upsert to Qdrant via the KnowledgeSearch's backing store
233
- // The KnowledgeSearch class does not expose hydrateItem directly — we
234
- // go through the embedder + vectorStore path manually.
235
- try {
236
- const text = `${title} ${observation}`;
237
- const vector = await this.embedder(text);
238
-
239
- // Access the backing QdrantVectorStore from KnowledgeSearch
240
- const store = (this.knowledgeSearch as unknown as {
241
- vectorStore?: { upsertKnowledge?: (id: string, vector: number[], payload: Record<string, unknown>) => Promise<void> };
242
- }).vectorStore;
243
-
244
- if (store?.upsertKnowledge) {
245
- await store.upsertKnowledge(id, vector, {
246
- id,
247
- repo_id: repoId,
248
- category,
249
- title,
250
- content: observation,
251
- stack_tags: [],
252
- confidence: 0.6,
253
- source: 'agent',
254
- source_phase: phase ?? null,
255
- source_agent: agent ?? null,
256
- sharing: 'private',
257
- created_at: now,
258
- updated_at: now,
259
- accessed_at: now,
260
- access_count: 0,
261
- });
262
- logger.debug('PatternExtractor: item upserted to Qdrant', { id });
263
- } else {
264
- logger.debug('PatternExtractor: Qdrant upsert unavailable, YAML only', { id });
265
- }
266
- } catch (err) {
267
- // Non-fatal: YAML write succeeded, Qdrant will be synced on next hydration
268
- logger.warn('PatternExtractor: Qdrant upsert failed (YAML write succeeded)', {
269
- id,
270
- error: String(err),
271
- });
272
- }
273
-
274
- logger.info('PatternExtractor: knowledge item promoted', {
275
- id,
276
- category,
277
- title,
278
- repoId,
279
- });
280
-
281
- return item;
282
- }
283
-
284
- // ---------------------------------------------------------------------------
285
- // Private helpers
286
- // ---------------------------------------------------------------------------
287
-
288
- /**
289
- * Check whether the observation text is semantically too close to an
290
- * existing knowledge item (similarity > 0.92). If so, we skip promotion
291
- * to avoid building up near-duplicate entries over many projects.
292
- */
293
- private async isDuplicateKnowledge(
294
- observation: string,
295
- category: KnowledgeCategory,
296
- ): Promise<boolean> {
297
- try {
298
- const results = await this.knowledgeSearch.search(observation, {
299
- category,
300
- limit: 1,
301
- min_confidence: 0.0,
302
- });
303
-
304
- return results.length > 0 && results[0]!.relevance_score > 0.92;
305
- } catch {
306
- // If search fails, assume not duplicate so we don't lose knowledge
307
- return false;
308
- }
309
- }
310
- }
311
-
312
- // ---------------------------------------------------------------------------
313
- // Internal utilities
314
- // ---------------------------------------------------------------------------
315
-
316
- function buildObservationText(step: TrajectoryStepRow): string {
317
- const parts: string[] = [step.action];
318
- if (step.result) parts.push(step.result);
319
- return parts.join('\n').trim();
320
- }
321
-
322
- function classifyCategory(step: TrajectoryStepRow): KnowledgeCategory {
323
- const text = `${step.action} ${step.result ?? ''}`.toLowerCase();
324
- const gotchaScore = GOTCHA_KEYWORDS.filter((kw) => text.includes(kw)).length;
325
- const patternScore = PATTERN_KEYWORDS.filter((kw) => text.includes(kw)).length;
326
-
327
- if (gotchaScore > patternScore) return 'gotcha';
328
- if (patternScore > gotchaScore) return 'pattern';
329
-
330
- // If quality score is high, lean toward "pattern" (positive finding)
331
- if (step.quality_score !== null && step.quality_score >= QUALITY_THRESHOLD) {
332
- return 'pattern';
333
- }
334
-
335
- return 'gotcha';
336
- }
337
-
338
- function categoryPrefix(category: KnowledgeCategory): string {
339
- switch (category) {
340
- case 'gotcha': return 'gotcha';
341
- case 'pattern': return 'pattern';
342
- case 'decision': return 'decision';
343
- case 'convention': return 'convention';
344
- }
345
- }
346
-
347
- function deriveTitle(observation: string, category: KnowledgeCategory): string {
348
- // Use the first sentence (up to 80 chars) as the title
349
- const firstSentence = observation.split(/[.!?\n]/)[0]?.trim() ?? observation;
350
- const truncated = firstSentence.length > 80
351
- ? firstSentence.slice(0, 77) + '...'
352
- : firstSentence;
353
-
354
- // Prefix with category for clarity when browsing YAML files
355
- const prefix = category.charAt(0).toUpperCase() + category.slice(1);
356
- return `${prefix}: ${truncated}`;
357
- }
358
-
359
- // Re-export keyword arrays so callers (e.g., tests) can reason about classification
360
- export { GOTCHA_KEYWORDS, PATTERN_KEYWORDS, QUALITY_THRESHOLD };
@@ -1,268 +0,0 @@
1
- // TrajectoryRecorder — B10
2
- //
3
- // Server-side automatic trajectory recording. Agents do NOT call this directly
4
- // — the pipeline tool handlers call startTrajectory() and recordStep() as a
5
- // side effect of processing forge.start_* and forge.submit_* tool calls.
6
- //
7
- // Storage: SQLite `trajectories` and `trajectory_steps` tables.
8
- // All operations are synchronous (better-sqlite3). No async needed.
9
- //
10
- // Design notes:
11
- // - startTrajectory() is safe to call multiple times for the same
12
- // (projectId, phase, agent) tuple — it creates a fresh trajectory each
13
- // time, returning the new ID. Old active trajectories for the same project
14
- // are left in the DB as historical records.
15
- // - recordStep() silently returns if `trajectoryId` is not found. This
16
- // prevents a missing trajectory from crashing a tool handler.
17
- // - completeTrajectory() and failTrajectory() are idempotent — they only
18
- // update rows whose status is 'active'.
19
-
20
- import { randomUUID } from 'node:crypto';
21
- import type { PipelineDB } from '../storage/sqlite.js';
22
- import type {
23
- Trajectory,
24
- TrajectoryRow,
25
- TrajectoryStep,
26
- TrajectoryStepRow,
27
- TrajectoryStepMetadata,
28
- PipelinePhase,
29
- } from '../util/types.js';
30
- import { logger } from '../util/logger.js';
31
-
32
- // ---------------------------------------------------------------------------
33
- // Row deserializers
34
- // ---------------------------------------------------------------------------
35
-
36
- function rowToTrajectory(row: TrajectoryRow): Trajectory {
37
- return {
38
- id: row.id,
39
- projectId: row.project_id,
40
- phase: row.phase as PipelinePhase,
41
- agent: row.agent,
42
- status: row.status,
43
- startedAt: row.started_at,
44
- completedAt: row.completed_at ?? null,
45
- success: row.success === null ? null : row.success === 1,
46
- feedback: row.feedback ?? null,
47
- };
48
- }
49
-
50
- function rowToStep(row: TrajectoryStepRow): TrajectoryStep {
51
- let metadata: TrajectoryStepMetadata = {};
52
- try {
53
- metadata = JSON.parse(row.metadata) as TrajectoryStepMetadata;
54
- } catch {
55
- metadata = {};
56
- }
57
- return {
58
- id: row.id,
59
- trajectoryId: row.trajectory_id,
60
- action: row.action,
61
- result: row.result ?? null,
62
- qualityScore: row.quality_score ?? null,
63
- metadata,
64
- createdAt: row.created_at,
65
- };
66
- }
67
-
68
- // ---------------------------------------------------------------------------
69
- // TrajectoryRecorder
70
- // ---------------------------------------------------------------------------
71
-
72
- export class TrajectoryRecorder {
73
- constructor(private readonly db: PipelineDB) {}
74
-
75
- /**
76
- * Start a new trajectory for the given project / phase / agent.
77
- * Returns the UUID of the newly created trajectory.
78
- *
79
- * Side-effect: any previously active trajectory for this project is NOT
80
- * auto-closed — they are left as historical data. The caller is responsible
81
- * for calling completeTrajectory() or failTrajectory() when appropriate.
82
- */
83
- startTrajectory(projectId: string, phase: PipelinePhase, agent: string): string {
84
- const id = randomUUID();
85
- const now = Date.now();
86
-
87
- this.db.run(
88
- `INSERT INTO trajectories (id, project_id, phase, agent, status, started_at, completed_at, success, feedback)
89
- VALUES (?, ?, ?, ?, 'active', ?, NULL, NULL, NULL)`,
90
- [id, projectId, phase, agent, now],
91
- );
92
-
93
- logger.debug('TrajectoryRecorder: trajectory started', {
94
- id,
95
- projectId,
96
- phase,
97
- agent,
98
- });
99
-
100
- return id;
101
- }
102
-
103
- /**
104
- * Record a step in an existing trajectory.
105
- * Silently returns when the trajectoryId does not exist in the DB — this
106
- * prevents missing trajectories from surfacing as user-facing errors.
107
- */
108
- recordStep(
109
- trajectoryId: string,
110
- action: string,
111
- result?: string,
112
- qualityScore?: number,
113
- metadata?: Record<string, unknown>,
114
- ): void {
115
- const now = Date.now();
116
-
117
- try {
118
- this.db.run(
119
- `INSERT INTO trajectory_steps (trajectory_id, action, result, quality_score, metadata, created_at)
120
- VALUES (?, ?, ?, ?, ?, ?)`,
121
- [
122
- trajectoryId,
123
- action,
124
- result ?? null,
125
- qualityScore ?? null,
126
- JSON.stringify(metadata ?? {}),
127
- now,
128
- ],
129
- );
130
-
131
- logger.debug('TrajectoryRecorder: step recorded', {
132
- trajectoryId,
133
- action: action.slice(0, 80),
134
- qualityScore,
135
- });
136
- } catch (err) {
137
- // Non-fatal — step recording must never crash the tool handler
138
- logger.warn('TrajectoryRecorder: failed to record step (non-fatal)', {
139
- trajectoryId,
140
- error: String(err),
141
- });
142
- }
143
- }
144
-
145
- /**
146
- * Mark a trajectory as successfully completed.
147
- * Idempotent — only updates rows with status='active'.
148
- */
149
- completeTrajectory(trajectoryId: string, feedback?: string): void {
150
- this.db.run(
151
- `UPDATE trajectories
152
- SET status = 'complete', completed_at = ?, success = 1, feedback = ?
153
- WHERE id = ? AND status = 'active'`,
154
- [Date.now(), feedback ?? null, trajectoryId],
155
- );
156
- logger.debug('TrajectoryRecorder: trajectory completed', { trajectoryId });
157
- }
158
-
159
- /**
160
- * Mark a trajectory as failed.
161
- * Idempotent — only updates rows with status='active'.
162
- */
163
- failTrajectory(trajectoryId: string, feedback?: string): void {
164
- this.db.run(
165
- `UPDATE trajectories
166
- SET status = 'failed', completed_at = ?, success = 0, feedback = ?
167
- WHERE id = ? AND status = 'active'`,
168
- [Date.now(), feedback ?? null, trajectoryId],
169
- );
170
- logger.debug('TrajectoryRecorder: trajectory failed', { trajectoryId });
171
- }
172
-
173
- /**
174
- * Return the most recent active trajectory for a project, or null.
175
- * "Active" means status = 'active'.
176
- */
177
- getActiveTrajectory(projectId: string): Trajectory | null {
178
- const row = this.db.get<TrajectoryRow>(
179
- `SELECT * FROM trajectories
180
- WHERE project_id = ? AND status = 'active'
181
- ORDER BY started_at DESC LIMIT 1`,
182
- [projectId],
183
- );
184
- return row ? rowToTrajectory(row) : null;
185
- }
186
-
187
- /**
188
- * Return all trajectories for a project, ordered by started_at ascending.
189
- */
190
- getTrajectories(projectId: string): Trajectory[] {
191
- const rows = this.db.all<TrajectoryRow>(
192
- `SELECT * FROM trajectories WHERE project_id = ? ORDER BY started_at ASC`,
193
- [projectId],
194
- );
195
- return rows.map(rowToTrajectory);
196
- }
197
-
198
- /**
199
- * Return all steps for a trajectory, ordered by created_at ascending.
200
- */
201
- getSteps(trajectoryId: string): TrajectoryStep[] {
202
- const rows = this.db.all<TrajectoryStepRow>(
203
- `SELECT * FROM trajectory_steps WHERE trajectory_id = ? ORDER BY created_at ASC`,
204
- [trajectoryId],
205
- );
206
- return rows.map(rowToStep);
207
- }
208
-
209
- /**
210
- * Produce a summary of all trajectory activity for a project.
211
- * Used by the knowledge_collection phase to feed the PatternExtractor.
212
- */
213
- getSummary(projectId: string): {
214
- totalSteps: number;
215
- phasesCompleted: number;
216
- cycles: Record<string, number>;
217
- durationMinutes: number;
218
- } {
219
- const trajectories = this.getTrajectories(projectId);
220
-
221
- if (trajectories.length === 0) {
222
- return {
223
- totalSteps: 0,
224
- phasesCompleted: 0,
225
- cycles: {},
226
- durationMinutes: 0,
227
- };
228
- }
229
-
230
- // Count total steps across all trajectories
231
- const totalStepsRow = this.db.get<{ cnt: number }>(
232
- `SELECT COUNT(*) as cnt FROM trajectory_steps ts
233
- JOIN trajectories t ON ts.trajectory_id = t.id
234
- WHERE t.project_id = ?`,
235
- [projectId],
236
- );
237
- const totalSteps = totalStepsRow?.cnt ?? 0;
238
-
239
- // Count completed phases
240
- const phasesCompleted = trajectories.filter(
241
- (t) => t.status === 'complete' && t.success === true,
242
- ).length;
243
-
244
- // Count cycles: phases that appear more than once in completed trajectories
245
- const phaseCompletions = trajectories
246
- .filter((t) => t.status === 'complete')
247
- .reduce<Record<string, number>>((acc, t) => {
248
- acc[t.phase] = (acc[t.phase] ?? 0) + 1;
249
- return acc;
250
- }, {});
251
-
252
- const cycles: Record<string, number> = {};
253
- for (const [phase, count] of Object.entries(phaseCompletions)) {
254
- if (count > 1) {
255
- cycles[phase] = count - 1; // cycles = extra runs beyond the first
256
- }
257
- }
258
-
259
- // Duration: from first startedAt to last completedAt (or now if still active)
260
- const firstStart = Math.min(...trajectories.map((t) => t.startedAt));
261
- const lastEnd = Math.max(
262
- ...trajectories.map((t) => t.completedAt ?? Date.now()),
263
- );
264
- const durationMinutes = Math.round((lastEnd - firstStart) / 60_000);
265
-
266
- return { totalSteps, phasesCompleted, cycles, durationMinutes };
267
- }
268
- }
File without changes