@soleri/core 9.9.0 → 9.11.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 (55) hide show
  1. package/dist/brain/intelligence.d.ts.map +1 -1
  2. package/dist/brain/intelligence.js +4 -0
  3. package/dist/brain/intelligence.js.map +1 -1
  4. package/dist/brain/types.d.ts +1 -1
  5. package/dist/brain/types.d.ts.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/paths.d.ts +10 -0
  11. package/dist/paths.d.ts.map +1 -1
  12. package/dist/paths.js +41 -2
  13. package/dist/paths.js.map +1 -1
  14. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  15. package/dist/planning/plan-lifecycle.js +4 -2
  16. package/dist/planning/plan-lifecycle.js.map +1 -1
  17. package/dist/planning/planner-types.d.ts +1 -1
  18. package/dist/planning/planner-types.d.ts.map +1 -1
  19. package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
  20. package/dist/runtime/admin-setup-ops.js +29 -4
  21. package/dist/runtime/admin-setup-ops.js.map +1 -1
  22. package/dist/runtime/capture-ops.js +2 -2
  23. package/dist/runtime/capture-ops.js.map +1 -1
  24. package/dist/runtime/claude-md-helpers.d.ts +11 -0
  25. package/dist/runtime/claude-md-helpers.d.ts.map +1 -1
  26. package/dist/runtime/claude-md-helpers.js +18 -0
  27. package/dist/runtime/claude-md-helpers.js.map +1 -1
  28. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  29. package/dist/runtime/facades/memory-facade.js +2 -1
  30. package/dist/runtime/facades/memory-facade.js.map +1 -1
  31. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  32. package/dist/runtime/orchestrate-ops.js +6 -3
  33. package/dist/runtime/orchestrate-ops.js.map +1 -1
  34. package/dist/runtime/quality-signals.d.ts +6 -1
  35. package/dist/runtime/quality-signals.d.ts.map +1 -1
  36. package/dist/runtime/quality-signals.js +41 -5
  37. package/dist/runtime/quality-signals.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/__tests__/paths.test.ts +31 -0
  40. package/src/brain/intelligence.ts +4 -0
  41. package/src/brain/types.ts +6 -1
  42. package/src/index.ts +1 -0
  43. package/src/paths.ts +42 -2
  44. package/src/planning/plan-lifecycle.ts +5 -2
  45. package/src/planning/planner-types.ts +1 -1
  46. package/src/planning/planner.test.ts +71 -0
  47. package/src/runtime/admin-setup-ops.ts +31 -3
  48. package/src/runtime/capture-ops.ts +2 -2
  49. package/src/runtime/claude-md-helpers.test.ts +81 -0
  50. package/src/runtime/claude-md-helpers.ts +25 -0
  51. package/src/runtime/facades/memory-facade.ts +2 -1
  52. package/src/runtime/orchestrate-ops.test.ts +51 -2
  53. package/src/runtime/orchestrate-ops.ts +12 -3
  54. package/src/runtime/quality-signals.test.ts +182 -8
  55. package/src/runtime/quality-signals.ts +44 -5
@@ -12,6 +12,7 @@ import {
12
12
  composeIntegrationSection,
13
13
  buildInjectionContent,
14
14
  injectEngineRulesBlock,
15
+ removeEngineRulesFromGlobal,
15
16
  } from './claude-md-helpers.js';
16
17
  import type { AgentRuntimeConfig } from './types.js';
17
18
 
@@ -186,4 +187,84 @@ describe('injectEngineRulesBlock', () => {
186
187
  expect(result).toContain('AFTER');
187
188
  expect(result).toContain('REPLACED');
188
189
  });
190
+
191
+ it('handles empty content by appending rules', () => {
192
+ const result = injectEngineRulesBlock('', 'RULES');
193
+ expect(result).toContain('RULES');
194
+ });
195
+
196
+ it('is idempotent — double injection replaces cleanly', () => {
197
+ const first = injectEngineRulesBlock('# File', `${RULES_START}\nV1\n${RULES_END}`);
198
+ const second = injectEngineRulesBlock(first, `${RULES_START}\nV2\n${RULES_END}`);
199
+ expect(second).toContain('V2');
200
+ expect(second).not.toContain('V1');
201
+ // Should have exactly one start marker
202
+ const startCount = (second.match(/<!-- soleri:engine-rules -->/g) || []).length;
203
+ expect(startCount).toBe(1);
204
+ });
205
+ });
206
+
207
+ describe('removeEngineRulesFromGlobal', () => {
208
+ const RULES_START = '<!-- soleri:engine-rules -->';
209
+ const RULES_END = '<!-- /soleri:engine-rules -->';
210
+
211
+ it('returns unchanged content when no engine rules present', () => {
212
+ const content = '# Global CLAUDE.md\n\nSome user content.';
213
+ const { cleaned, removed } = removeEngineRulesFromGlobal(content);
214
+ expect(removed).toBe(false);
215
+ expect(cleaned).toBe(content);
216
+ });
217
+
218
+ it('removes engine rules block from content', () => {
219
+ const content = `# Global\n\n${RULES_START}\nEngine rules here\n${RULES_END}\n\nUser content`;
220
+ const { cleaned, removed } = removeEngineRulesFromGlobal(content);
221
+ expect(removed).toBe(true);
222
+ expect(cleaned).not.toContain('Engine rules here');
223
+ expect(cleaned).not.toContain(RULES_START);
224
+ expect(cleaned).not.toContain(RULES_END);
225
+ expect(cleaned).toContain('# Global');
226
+ expect(cleaned).toContain('User content');
227
+ });
228
+
229
+ it('handles engine rules at end of file', () => {
230
+ const content = `# Global\n\n${RULES_START}\nRules\n${RULES_END}`;
231
+ const { cleaned, removed } = removeEngineRulesFromGlobal(content);
232
+ expect(removed).toBe(true);
233
+ expect(cleaned).toContain('# Global');
234
+ expect(cleaned).not.toContain('Rules');
235
+ });
236
+
237
+ it('handles engine rules at start of file', () => {
238
+ const content = `${RULES_START}\nRules\n${RULES_END}\n\n# Global`;
239
+ const { cleaned, removed } = removeEngineRulesFromGlobal(content);
240
+ expect(removed).toBe(true);
241
+ expect(cleaned).toContain('# Global');
242
+ expect(cleaned).not.toContain(RULES_START);
243
+ });
244
+
245
+ it('preserves agent blocks when removing engine rules', () => {
246
+ const content = [
247
+ '# Global',
248
+ '',
249
+ '<!-- agent:mybot:mode -->',
250
+ 'Agent content',
251
+ '<!-- /agent:mybot:mode -->',
252
+ '',
253
+ RULES_START,
254
+ 'Engine rules',
255
+ RULES_END,
256
+ ].join('\n');
257
+ const { cleaned, removed } = removeEngineRulesFromGlobal(content);
258
+ expect(removed).toBe(true);
259
+ expect(cleaned).toContain('<!-- agent:mybot:mode -->');
260
+ expect(cleaned).toContain('Agent content');
261
+ expect(cleaned).not.toContain('Engine rules');
262
+ });
263
+
264
+ it('returns non-empty string for content that is only engine rules', () => {
265
+ const content = `${RULES_START}\nOnly rules\n${RULES_END}`;
266
+ const { cleaned, removed } = removeEngineRulesFromGlobal(content);
267
+ expect(removed).toBe(true);
268
+ expect(cleaned.length).toBeGreaterThan(0);
269
+ });
189
270
  });
@@ -216,3 +216,28 @@ export function injectEngineRulesBlock(content: string, engineRulesContent: stri
216
216
  // Append
217
217
  return content.trimEnd() + '\n\n' + engineRulesContent + '\n';
218
218
  }
219
+
220
+ /**
221
+ * Remove engine rules block from content.
222
+ * Used during self-healing to strip engine rules from global CLAUDE.md
223
+ * when they were incorrectly injected there (they belong in _engine.md).
224
+ *
225
+ * Returns the content without the engine rules block, or unchanged if no block found.
226
+ */
227
+ export function removeEngineRulesFromGlobal(content: string): {
228
+ cleaned: string;
229
+ removed: boolean;
230
+ } {
231
+ if (!hasEngineRules(content)) {
232
+ return { cleaned: content, removed: false };
233
+ }
234
+
235
+ const startIdx = content.indexOf(ENGINE_RULES_START);
236
+ const endIdx = content.indexOf(ENGINE_RULES_END);
237
+
238
+ const before = content.slice(0, startIdx).trimEnd();
239
+ const after = content.slice(endIdx + ENGINE_RULES_END.length).trimStart();
240
+
241
+ const cleaned = before + (before && after ? '\n\n' : '') + after;
242
+ return { cleaned: cleaned || '\n', removed: true };
243
+ }
@@ -151,7 +151,8 @@ export function createMemoryFacadeOps(runtime: AgentRuntime): OpDefinition[] {
151
151
  }),
152
152
  handler: async (params) => {
153
153
  const { resolve } = await import('node:path');
154
- const projectPath = resolve((params.projectPath as string) ?? '.');
154
+ const { findProjectRoot } = await import('../../paths.js');
155
+ const projectPath = findProjectRoot(resolve((params.projectPath as string) ?? '.'));
155
156
  const summary = (params.summary ?? params.conversationContext) as string;
156
157
  if (!summary) {
157
158
  return { captured: false, error: 'Either summary or conversationContext is required.' };
@@ -363,10 +363,10 @@ describe('createOrchestrateOps', () => {
363
363
  outcome: 'completed',
364
364
  })) as Record<string, unknown>;
365
365
 
366
- // Should complete successfully without evidenceReport
366
+ // Should complete successfully with evidenceReport: null
367
367
  expect(result).toHaveProperty('plan');
368
368
  expect(result).toHaveProperty('session');
369
- expect(result).not.toHaveProperty('evidenceReport');
369
+ expect(result.evidenceReport).toBeNull();
370
370
  });
371
371
 
372
372
  it('adds warning when evidence accuracy is below 50%', async () => {
@@ -395,6 +395,55 @@ describe('createOrchestrateOps', () => {
395
395
  const warnings = result.warnings as string[];
396
396
  expect(warnings.some((w) => w.includes('Low evidence accuracy (30%)'))).toBe(true);
397
397
  });
398
+
399
+ it('runs evidence collection for abandoned plans too', async () => {
400
+ const { collectGitEvidence } = await import('../planning/evidence-collector.js');
401
+ vi.mocked(collectGitEvidence).mockReturnValueOnce({
402
+ planId: 'plan-1',
403
+ planObjective: 'test',
404
+ accuracy: 60,
405
+ evidenceSources: ['git'],
406
+ taskEvidence: [
407
+ {
408
+ taskId: 't1',
409
+ taskTitle: 'Task 1',
410
+ plannedStatus: 'pending',
411
+ matchedFiles: [],
412
+ verdict: 'MISSING',
413
+ },
414
+ ],
415
+ unplannedChanges: [],
416
+ missingWork: [],
417
+ verificationGaps: [],
418
+ summary: '0/1 tasks verified by git evidence',
419
+ });
420
+
421
+ const op = findOp(ops, 'orchestrate_complete');
422
+ const result = (await op.handler({
423
+ planId: 'plan-1',
424
+ sessionId: 'session-1',
425
+ outcome: 'abandoned',
426
+ projectPath: '.',
427
+ })) as Record<string, unknown>;
428
+
429
+ expect(collectGitEvidence).toHaveBeenCalled();
430
+ expect(result).toHaveProperty('evidenceReport');
431
+ const report = result.evidenceReport as Record<string, unknown>;
432
+ expect(report.accuracy).toBe(60);
433
+ expect(Array.isArray(report.taskEvidence)).toBe(true);
434
+ });
435
+
436
+ it('returns evidenceReport as null when no plan is provided', async () => {
437
+ const op = findOp(ops, 'orchestrate_complete');
438
+ const result = (await op.handler({
439
+ sessionId: 'session-1',
440
+ outcome: 'completed',
441
+ summary: 'Direct task without a plan',
442
+ })) as Record<string, unknown>;
443
+
444
+ expect(result).toHaveProperty('evidenceReport');
445
+ expect(result.evidenceReport).toBeNull();
446
+ });
398
447
  });
399
448
 
400
449
  // ─── orchestrate_status ───────────────────────────────────────
@@ -43,7 +43,11 @@ import type { ImpactReport } from '../planning/impact-analyzer.js';
43
43
  import { collectGitEvidence } from '../planning/evidence-collector.js';
44
44
  import type { EvidenceReport } from '../planning/evidence-collector.js';
45
45
  import { recordPlanFeedback } from './plan-feedback-helper.js';
46
- import { analyzeQualitySignals, captureQualitySignals } from './quality-signals.js';
46
+ import {
47
+ analyzeQualitySignals,
48
+ captureQualitySignals,
49
+ buildFixTrailSummary,
50
+ } from './quality-signals.js';
47
51
 
48
52
  // ---------------------------------------------------------------------------
49
53
  // Intent detection — keyword-based mapping from prompt to intent
@@ -832,7 +836,7 @@ export function createOrchestrateOps(
832
836
 
833
837
  // Evidence-based reconciliation: cross-reference plan tasks against git diff
834
838
  let evidenceReport: EvidenceReport | null = null;
835
- if (planObj && outcome === 'completed') {
839
+ if (planObj) {
836
840
  try {
837
841
  evidenceReport = collectGitEvidence(
838
842
  planObj,
@@ -840,6 +844,9 @@ export function createOrchestrateOps(
840
844
  'main',
841
845
  );
842
846
  if (evidenceReport.accuracy < 50) {
847
+ console.error(
848
+ `[soleri] Evidence accuracy ${evidenceReport.accuracy}% — significant drift detected between plan and git state`,
849
+ );
843
850
  warnings.push(
844
851
  `Low evidence accuracy (${evidenceReport.accuracy}%) — plan tasks may not match git changes.`,
845
852
  );
@@ -873,6 +880,7 @@ export function createOrchestrateOps(
873
880
  }
874
881
 
875
882
  // End brain session — runs regardless of plan existence
883
+ const fixTrail = evidenceReport ? buildFixTrailSummary(evidenceReport) : undefined;
876
884
  const session = brainIntelligence.lifecycle({
877
885
  action: 'end',
878
886
  sessionId,
@@ -880,6 +888,7 @@ export function createOrchestrateOps(
880
888
  planOutcome: outcome,
881
889
  toolsUsed,
882
890
  filesModified,
891
+ ...(fixTrail ? { context: `Fix trail: ${fixTrail}` } : {}),
883
892
  });
884
893
 
885
894
  // Record brain feedback for vault entries referenced in plan decisions
@@ -974,7 +983,7 @@ export function createOrchestrateOps(
974
983
  extraction,
975
984
  epilogue: epilogueResult,
976
985
  ...(impactReport ? { impactAnalysis: impactReport } : {}),
977
- ...(evidenceReport ? { evidenceReport } : {}),
986
+ evidenceReport,
978
987
  ...(warnings.length > 0 ? { warnings } : {}),
979
988
  };
980
989
  },
@@ -1,5 +1,9 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { analyzeQualitySignals, captureQualitySignals } from './quality-signals.js';
2
+ import {
3
+ analyzeQualitySignals,
4
+ captureQualitySignals,
5
+ buildFixTrailSummary,
6
+ } from './quality-signals.js';
3
7
  import type { EvidenceReport } from '../planning/evidence-collector.js';
4
8
 
5
9
  // ---------------------------------------------------------------------------
@@ -104,7 +108,7 @@ describe('analyzeQualitySignals', () => {
104
108
  expect(result.antiPatterns).toHaveLength(0);
105
109
  });
106
110
 
107
- it('does not flag task with fixIterations === 2 (at threshold, not above)', () => {
111
+ it('flags task with fixIterations === 2 (at threshold)', () => {
108
112
  const report = makeReport({
109
113
  taskEvidence: [
110
114
  {
@@ -120,7 +124,28 @@ describe('analyzeQualitySignals', () => {
120
124
 
121
125
  const result = analyzeQualitySignals(report);
122
126
 
127
+ expect(result.antiPatterns).toHaveLength(1);
128
+ expect(result.antiPatterns[0].fixIterations).toBe(2);
129
+ });
130
+
131
+ it('does not flag task with fixIterations === 1 (below threshold)', () => {
132
+ const report = makeReport({
133
+ taskEvidence: [
134
+ {
135
+ taskId: 't4b',
136
+ taskTitle: 'Single retry task',
137
+ plannedStatus: 'completed',
138
+ matchedFiles: [],
139
+ verdict: 'DONE',
140
+ fixIterations: 1,
141
+ },
142
+ ],
143
+ });
144
+
145
+ const result = analyzeQualitySignals(report);
146
+
123
147
  expect(result.antiPatterns).toHaveLength(0);
148
+ expect(result.cleanTasks).toHaveLength(0);
124
149
  });
125
150
 
126
151
  it('detects scope creep from unplanned changes', () => {
@@ -199,9 +224,14 @@ describe('captureQualitySignals', () => {
199
224
  expect(entry.tags).toContain('auto-captured');
200
225
 
201
226
  expect(brain.recordFeedback).toHaveBeenCalledWith(
202
- 'quality-signal:rework:Fix login',
203
- 't1',
204
- 'dismissed',
227
+ expect.objectContaining({
228
+ query: 'Fix login',
229
+ entryId: 'plan-1',
230
+ action: 'dismissed',
231
+ confidence: 0.7,
232
+ source: 'evidence-quality',
233
+ reason: 'Task needed 3 fix iterations — high rework',
234
+ }),
205
235
  );
206
236
 
207
237
  expect(result.captured).toBe(1);
@@ -226,9 +256,14 @@ describe('captureQualitySignals', () => {
226
256
  const result = captureQualitySignals(analysis, vault, brain, 'plan-1');
227
257
 
228
258
  expect(brain.recordFeedback).toHaveBeenCalledWith(
229
- 'quality-signal:clean:Add feature',
230
- 't2',
231
- 'accepted',
259
+ expect.objectContaining({
260
+ query: 'Add feature',
261
+ entryId: 'plan-1',
262
+ action: 'accepted',
263
+ confidence: 0.9,
264
+ source: 'evidence-quality',
265
+ reason: 'Clean first-try completion — no rework needed',
266
+ }),
232
267
  );
233
268
  expect(result.feedback).toBe(1);
234
269
  expect(result.captured).toBe(0);
@@ -281,6 +316,83 @@ describe('captureQualitySignals', () => {
281
316
  expect(entry.severity).toBe('critical');
282
317
  });
283
318
 
319
+ it('records positive feedback with evidence-quality source for clean first-try tasks', () => {
320
+ const analysis = {
321
+ antiPatterns: [],
322
+ cleanTasks: [
323
+ {
324
+ taskId: 'clean-1',
325
+ taskTitle: 'Smooth task',
326
+ kind: 'clean' as const,
327
+ fixIterations: 0,
328
+ verdict: 'DONE',
329
+ },
330
+ ],
331
+ scopeCreep: [],
332
+ };
333
+
334
+ captureQualitySignals(analysis, vault, brain, 'plan-99');
335
+
336
+ expect(brain.recordFeedback).toHaveBeenCalledWith(
337
+ expect.objectContaining({
338
+ action: 'accepted',
339
+ confidence: 0.9,
340
+ source: 'evidence-quality',
341
+ entryId: 'plan-99',
342
+ }),
343
+ );
344
+ });
345
+
346
+ it('records negative feedback with evidence-quality source for high-rework tasks', () => {
347
+ const analysis = {
348
+ antiPatterns: [
349
+ {
350
+ taskId: 'rework-1',
351
+ taskTitle: 'Painful task',
352
+ kind: 'anti-pattern' as const,
353
+ fixIterations: 3,
354
+ verdict: 'DONE',
355
+ },
356
+ ],
357
+ cleanTasks: [],
358
+ scopeCreep: [],
359
+ };
360
+
361
+ captureQualitySignals(analysis, vault, brain, 'plan-99');
362
+
363
+ expect(brain.recordFeedback).toHaveBeenCalledWith(
364
+ expect.objectContaining({
365
+ action: 'dismissed',
366
+ confidence: 0.7,
367
+ source: 'evidence-quality',
368
+ reason: 'Task needed 3 fix iterations — high rework',
369
+ context: JSON.stringify({ taskId: 'rework-1', reworkCount: 3, verdict: 'DONE' }),
370
+ }),
371
+ );
372
+ });
373
+
374
+ it('does not record evidence-quality feedback for tasks with 1 fix iteration', () => {
375
+ // 1 fix iteration = neither clean (fixIterations !== 0) nor anti-pattern (< 2)
376
+ const analysis = analyzeQualitySignals(
377
+ makeReport({
378
+ taskEvidence: [
379
+ {
380
+ taskId: 't-mid',
381
+ taskTitle: 'Single retry',
382
+ plannedStatus: 'completed',
383
+ matchedFiles: [],
384
+ verdict: 'DONE',
385
+ fixIterations: 1,
386
+ },
387
+ ],
388
+ }),
389
+ );
390
+
391
+ captureQualitySignals(analysis, vault, brain, 'plan-99');
392
+
393
+ expect(brain.recordFeedback).not.toHaveBeenCalled();
394
+ });
395
+
284
396
  it('handles mixed signals correctly', () => {
285
397
  const analysis = {
286
398
  antiPatterns: [
@@ -310,3 +422,65 @@ describe('captureQualitySignals', () => {
310
422
  expect(result.feedback).toBe(2); // 1 dismissed + 1 accepted
311
423
  });
312
424
  });
425
+
426
+ // ---------------------------------------------------------------------------
427
+ // buildFixTrailSummary
428
+ // ---------------------------------------------------------------------------
429
+
430
+ describe('buildFixTrailSummary', () => {
431
+ it('returns summary string for tasks with rework iterations', () => {
432
+ const report = makeReport({
433
+ taskEvidence: [
434
+ {
435
+ taskId: 'a',
436
+ taskTitle: 'Task A',
437
+ plannedStatus: 'completed',
438
+ matchedFiles: [],
439
+ verdict: 'DONE',
440
+ fixIterations: 2,
441
+ },
442
+ {
443
+ taskId: 'b',
444
+ taskTitle: 'Task B',
445
+ plannedStatus: 'completed',
446
+ matchedFiles: [],
447
+ verdict: 'DONE',
448
+ fixIterations: 0,
449
+ },
450
+ {
451
+ taskId: 'c',
452
+ taskTitle: 'Task C',
453
+ plannedStatus: 'completed',
454
+ matchedFiles: [],
455
+ verdict: 'DONE',
456
+ fixIterations: 3,
457
+ },
458
+ ],
459
+ });
460
+
461
+ const summary = buildFixTrailSummary(report);
462
+ expect(summary).toBe('Task A: 2 fix iterations; Task C: 3 fix iterations');
463
+ });
464
+
465
+ it('returns undefined when no tasks have rework', () => {
466
+ const report = makeReport({
467
+ taskEvidence: [
468
+ {
469
+ taskId: 'a',
470
+ taskTitle: 'Clean',
471
+ plannedStatus: 'completed',
472
+ matchedFiles: [],
473
+ verdict: 'DONE',
474
+ fixIterations: 0,
475
+ },
476
+ ],
477
+ });
478
+
479
+ expect(buildFixTrailSummary(report)).toBeUndefined();
480
+ });
481
+
482
+ it('returns undefined for empty task evidence', () => {
483
+ const report = makeReport({ taskEvidence: [] });
484
+ expect(buildFixTrailSummary(report)).toBeUndefined();
485
+ });
486
+ });
@@ -34,8 +34,12 @@ export interface QualityAnalysis {
34
34
  // Thresholds
35
35
  // ---------------------------------------------------------------------------
36
36
 
37
- /** Tasks with more than this many fix iterations are flagged as anti-patterns. */
37
+ /** Tasks with this many or more fix iterations are flagged as anti-patterns. */
38
38
  const REWORK_THRESHOLD = 2;
39
+ /** Brain feedback confidence for clean first-try tasks. */
40
+ const CLEAN_TASK_CONFIDENCE = 0.9;
41
+ /** Brain feedback confidence for high-rework anti-pattern tasks. */
42
+ const REWORK_TASK_CONFIDENCE = 0.7;
39
43
 
40
44
  // ---------------------------------------------------------------------------
41
45
  // Analysis
@@ -44,7 +48,7 @@ const REWORK_THRESHOLD = 2;
44
48
  /**
45
49
  * Analyze an evidence report for quality signals.
46
50
  *
47
- * - fixIterations > 2 → anti-pattern (rework)
51
+ * - fixIterations >= 2 → anti-pattern (rework)
48
52
  * - fixIterations === 0 + verdict DONE → clean (first-pass success)
49
53
  * - unplannedChanges → scope-creep signals
50
54
  */
@@ -59,7 +63,7 @@ export function analyzeQualitySignals(
59
63
  for (const te of report.taskEvidence) {
60
64
  const iterations = te.fixIterations ?? 0;
61
65
 
62
- if (iterations > REWORK_THRESHOLD) {
66
+ if (iterations >= REWORK_THRESHOLD) {
63
67
  antiPatterns.push({
64
68
  taskId: te.taskId,
65
69
  taskTitle: te.taskTitle,
@@ -148,7 +152,19 @@ export function captureQualitySignals(
148
152
  // Record negative brain feedback for rework tasks
149
153
  for (const ap of analysis.antiPatterns) {
150
154
  try {
151
- brain.recordFeedback(`quality-signal:rework:${ap.taskTitle}`, ap.taskId, 'dismissed');
155
+ brain.recordFeedback({
156
+ query: ap.taskTitle,
157
+ entryId: planId,
158
+ action: 'dismissed',
159
+ confidence: REWORK_TASK_CONFIDENCE,
160
+ source: 'evidence-quality',
161
+ reason: `Task needed ${ap.fixIterations} fix iterations — high rework`,
162
+ context: JSON.stringify({
163
+ taskId: ap.taskId,
164
+ reworkCount: ap.fixIterations,
165
+ verdict: ap.verdict,
166
+ }),
167
+ });
152
168
  feedback++;
153
169
  } catch {
154
170
  // Best-effort
@@ -158,7 +174,14 @@ export function captureQualitySignals(
158
174
  // Record positive brain feedback for clean tasks
159
175
  for (const ct of analysis.cleanTasks) {
160
176
  try {
161
- brain.recordFeedback(`quality-signal:clean:${ct.taskTitle}`, ct.taskId, 'accepted');
177
+ brain.recordFeedback({
178
+ query: ct.taskTitle,
179
+ entryId: planId,
180
+ action: 'accepted',
181
+ confidence: CLEAN_TASK_CONFIDENCE,
182
+ source: 'evidence-quality',
183
+ reason: 'Clean first-try completion — no rework needed',
184
+ });
162
185
  feedback++;
163
186
  } catch {
164
187
  // Best-effort
@@ -167,3 +190,19 @@ export function captureQualitySignals(
167
190
 
168
191
  return { captured, skipped, feedback };
169
192
  }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Fix-trail summary for knowledge extraction
196
+ // ---------------------------------------------------------------------------
197
+
198
+ /**
199
+ * Build a human-readable fix-trail summary from an evidence report.
200
+ * Returns `undefined` when no tasks had rework iterations.
201
+ */
202
+ export function buildFixTrailSummary(report: EvidenceReport): string | undefined {
203
+ const entries = report.taskEvidence
204
+ .filter((te) => (te.fixIterations ?? 0) > 0)
205
+ .map((te) => `${te.taskTitle}: ${te.fixIterations} fix iterations`);
206
+
207
+ return entries.length > 0 ? entries.join('; ') : undefined;
208
+ }