@soleri/core 9.7.2 → 9.9.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.
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +11 -2
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/types.d.ts +1 -0
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/enforcement/adapters/index.d.ts +15 -0
- package/dist/enforcement/adapters/index.d.ts.map +1 -1
- package/dist/enforcement/adapters/index.js +38 -0
- package/dist/enforcement/adapters/index.js.map +1 -1
- package/dist/enforcement/adapters/opencode.d.ts +21 -0
- package/dist/enforcement/adapters/opencode.d.ts.map +1 -0
- package/dist/enforcement/adapters/opencode.js +115 -0
- package/dist/enforcement/adapters/opencode.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +2 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +4 -0
- package/dist/paths.js.map +1 -1
- package/dist/planning/evidence-collector.d.ts +2 -0
- package/dist/planning/evidence-collector.d.ts.map +1 -1
- package/dist/planning/evidence-collector.js +7 -2
- package/dist/planning/evidence-collector.js.map +1 -1
- package/dist/planning/gap-patterns.d.ts.map +1 -1
- package/dist/planning/gap-patterns.js +4 -1
- package/dist/planning/gap-patterns.js.map +1 -1
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +5 -0
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/planning/planner-types.d.ts +2 -0
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +14 -6
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
- package/dist/runtime/facades/curator-facade.js +52 -4
- package/dist/runtime/facades/curator-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts +12 -0
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +141 -1
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/quality-signals.d.ts +42 -0
- package/dist/runtime/quality-signals.d.ts.map +1 -0
- package/dist/runtime/quality-signals.js +124 -0
- package/dist/runtime/quality-signals.js.map +1 -0
- package/dist/skills/trust-classifier.js +1 -1
- package/dist/skills/trust-classifier.js.map +1 -1
- package/dist/vault/vault-markdown-sync.d.ts +5 -2
- package/dist/vault/vault-markdown-sync.d.ts.map +1 -1
- package/dist/vault/vault-markdown-sync.js +13 -2
- package/dist/vault/vault-markdown-sync.js.map +1 -1
- package/dist/workflows/index.d.ts +6 -0
- package/dist/workflows/index.d.ts.map +1 -0
- package/dist/workflows/index.js +5 -0
- package/dist/workflows/index.js.map +1 -0
- package/dist/workflows/workflow-loader.d.ts +83 -0
- package/dist/workflows/workflow-loader.d.ts.map +1 -0
- package/dist/workflows/workflow-loader.js +207 -0
- package/dist/workflows/workflow-loader.js.map +1 -0
- package/package.json +1 -1
- package/src/brain/intelligence.ts +15 -2
- package/src/brain/types.ts +1 -0
- package/src/enforcement/adapters/index.ts +45 -0
- package/src/enforcement/adapters/opencode.test.ts +406 -0
- package/src/enforcement/adapters/opencode.ts +153 -0
- package/src/index.ts +19 -0
- package/src/paths.ts +5 -0
- package/src/planning/evidence-collector.test.ts +95 -0
- package/src/planning/evidence-collector.ts +11 -0
- package/src/planning/gap-patterns.ts +7 -3
- package/src/planning/plan-lifecycle.test.ts +49 -0
- package/src/planning/plan-lifecycle.ts +5 -0
- package/src/planning/planner-types.ts +2 -0
- package/src/runtime/capture-ops.test.ts +58 -1
- package/src/runtime/capture-ops.ts +15 -4
- package/src/runtime/facades/curator-facade.test.ts +87 -9
- package/src/runtime/facades/curator-facade.ts +60 -4
- package/src/runtime/orchestrate-ops.test.ts +78 -1
- package/src/runtime/orchestrate-ops.ts +175 -1
- package/src/runtime/orchestrate-status-readiness.test.ts +162 -0
- package/src/runtime/quality-signals.test.ts +312 -0
- package/src/runtime/quality-signals.ts +169 -0
- package/src/skills/trust-classifier.ts +1 -1
- package/src/vault/vault-markdown-sync.test.ts +40 -0
- package/src/vault/vault-markdown-sync.ts +16 -3
- package/src/workflows/index.ts +12 -0
- package/src/workflows/orchestrate-integration.test.ts +166 -0
- package/src/workflows/workflow-loader.test.ts +149 -0
- package/src/workflows/workflow-loader.ts +238 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { analyzeQualitySignals, captureQualitySignals } from './quality-signals.js';
|
|
3
|
+
import type { EvidenceReport } from '../planning/evidence-collector.js';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
function makeReport(overrides: Partial<EvidenceReport> = {}): EvidenceReport {
|
|
10
|
+
return {
|
|
11
|
+
planId: 'plan-test',
|
|
12
|
+
planObjective: 'Test plan',
|
|
13
|
+
accuracy: 80,
|
|
14
|
+
evidenceSources: ['git'],
|
|
15
|
+
taskEvidence: [],
|
|
16
|
+
unplannedChanges: [],
|
|
17
|
+
missingWork: [],
|
|
18
|
+
verificationGaps: [],
|
|
19
|
+
summary: '',
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeVault() {
|
|
25
|
+
return {
|
|
26
|
+
search: vi.fn().mockReturnValue([]),
|
|
27
|
+
add: vi.fn(),
|
|
28
|
+
} as unknown as ReturnType<
|
|
29
|
+
(typeof import('../vault/vault.js'))['Vault']['prototype']['search']
|
|
30
|
+
> & { add: ReturnType<typeof vi.fn>; search: ReturnType<typeof vi.fn> };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeBrain() {
|
|
34
|
+
return {
|
|
35
|
+
recordFeedback: vi.fn(),
|
|
36
|
+
} as unknown as { recordFeedback: ReturnType<typeof vi.fn> };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// analyzeQualitySignals
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
describe('analyzeQualitySignals', () => {
|
|
44
|
+
it('detects anti-pattern when fixIterations > 2', () => {
|
|
45
|
+
const report = makeReport({
|
|
46
|
+
taskEvidence: [
|
|
47
|
+
{
|
|
48
|
+
taskId: 't1',
|
|
49
|
+
taskTitle: 'Fix login bug',
|
|
50
|
+
plannedStatus: 'completed',
|
|
51
|
+
matchedFiles: [],
|
|
52
|
+
verdict: 'DONE',
|
|
53
|
+
fixIterations: 3,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const result = analyzeQualitySignals(report);
|
|
59
|
+
|
|
60
|
+
expect(result.antiPatterns).toHaveLength(1);
|
|
61
|
+
expect(result.antiPatterns[0].taskId).toBe('t1');
|
|
62
|
+
expect(result.antiPatterns[0].kind).toBe('anti-pattern');
|
|
63
|
+
expect(result.antiPatterns[0].fixIterations).toBe(3);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('detects clean task when fixIterations === 0 and verdict DONE', () => {
|
|
67
|
+
const report = makeReport({
|
|
68
|
+
taskEvidence: [
|
|
69
|
+
{
|
|
70
|
+
taskId: 't2',
|
|
71
|
+
taskTitle: 'Add feature',
|
|
72
|
+
plannedStatus: 'completed',
|
|
73
|
+
matchedFiles: [],
|
|
74
|
+
verdict: 'DONE',
|
|
75
|
+
fixIterations: 0,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const result = analyzeQualitySignals(report);
|
|
81
|
+
|
|
82
|
+
expect(result.cleanTasks).toHaveLength(1);
|
|
83
|
+
expect(result.cleanTasks[0].taskId).toBe('t2');
|
|
84
|
+
expect(result.cleanTasks[0].kind).toBe('clean');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('does not flag task with fixIterations === 0 but verdict PARTIAL', () => {
|
|
88
|
+
const report = makeReport({
|
|
89
|
+
taskEvidence: [
|
|
90
|
+
{
|
|
91
|
+
taskId: 't3',
|
|
92
|
+
taskTitle: 'Partial work',
|
|
93
|
+
plannedStatus: 'in_progress',
|
|
94
|
+
matchedFiles: [],
|
|
95
|
+
verdict: 'PARTIAL',
|
|
96
|
+
fixIterations: 0,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const result = analyzeQualitySignals(report);
|
|
102
|
+
|
|
103
|
+
expect(result.cleanTasks).toHaveLength(0);
|
|
104
|
+
expect(result.antiPatterns).toHaveLength(0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('does not flag task with fixIterations === 2 (at threshold, not above)', () => {
|
|
108
|
+
const report = makeReport({
|
|
109
|
+
taskEvidence: [
|
|
110
|
+
{
|
|
111
|
+
taskId: 't4',
|
|
112
|
+
taskTitle: 'Borderline task',
|
|
113
|
+
plannedStatus: 'completed',
|
|
114
|
+
matchedFiles: [],
|
|
115
|
+
verdict: 'DONE',
|
|
116
|
+
fixIterations: 2,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const result = analyzeQualitySignals(report);
|
|
122
|
+
|
|
123
|
+
expect(result.antiPatterns).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('detects scope creep from unplanned changes', () => {
|
|
127
|
+
const report = makeReport({
|
|
128
|
+
unplannedChanges: [
|
|
129
|
+
{
|
|
130
|
+
file: { path: 'src/extra.ts', status: 'added' },
|
|
131
|
+
possibleReason: 'unplanned scope',
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const result = analyzeQualitySignals(report);
|
|
137
|
+
|
|
138
|
+
expect(result.scopeCreep).toHaveLength(1);
|
|
139
|
+
expect(result.scopeCreep[0].kind).toBe('scope-creep');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('handles undefined fixIterations as 0', () => {
|
|
143
|
+
const report = makeReport({
|
|
144
|
+
taskEvidence: [
|
|
145
|
+
{
|
|
146
|
+
taskId: 't5',
|
|
147
|
+
taskTitle: 'No iterations field',
|
|
148
|
+
plannedStatus: 'completed',
|
|
149
|
+
matchedFiles: [],
|
|
150
|
+
verdict: 'DONE',
|
|
151
|
+
// fixIterations omitted
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const result = analyzeQualitySignals(report);
|
|
157
|
+
|
|
158
|
+
expect(result.cleanTasks).toHaveLength(1);
|
|
159
|
+
expect(result.antiPatterns).toHaveLength(0);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// captureQualitySignals
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
describe('captureQualitySignals', () => {
|
|
168
|
+
let vault: ReturnType<typeof makeVault>;
|
|
169
|
+
let brain: ReturnType<typeof makeBrain>;
|
|
170
|
+
|
|
171
|
+
beforeEach(() => {
|
|
172
|
+
vault = makeVault();
|
|
173
|
+
brain = makeBrain();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('captures anti-pattern to vault and records negative brain feedback', () => {
|
|
177
|
+
const analysis = {
|
|
178
|
+
antiPatterns: [
|
|
179
|
+
{
|
|
180
|
+
taskId: 't1',
|
|
181
|
+
taskTitle: 'Fix login',
|
|
182
|
+
kind: 'anti-pattern' as const,
|
|
183
|
+
fixIterations: 3,
|
|
184
|
+
verdict: 'DONE',
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
cleanTasks: [],
|
|
188
|
+
scopeCreep: [],
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const result = captureQualitySignals(analysis, vault, brain, 'plan-1');
|
|
192
|
+
|
|
193
|
+
expect(vault.add).toHaveBeenCalledTimes(1);
|
|
194
|
+
const entry = vault.add.mock.calls[0][0];
|
|
195
|
+
expect(entry.type).toBe('anti-pattern');
|
|
196
|
+
expect(entry.severity).toBe('warning');
|
|
197
|
+
expect(entry.tags).toContain('rework');
|
|
198
|
+
expect(entry.tags).toContain('fix-trail');
|
|
199
|
+
expect(entry.tags).toContain('auto-captured');
|
|
200
|
+
|
|
201
|
+
expect(brain.recordFeedback).toHaveBeenCalledWith(
|
|
202
|
+
'quality-signal:rework:Fix login',
|
|
203
|
+
't1',
|
|
204
|
+
'dismissed',
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(result.captured).toBe(1);
|
|
208
|
+
expect(result.feedback).toBe(1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('records positive brain feedback for clean tasks', () => {
|
|
212
|
+
const analysis = {
|
|
213
|
+
antiPatterns: [],
|
|
214
|
+
cleanTasks: [
|
|
215
|
+
{
|
|
216
|
+
taskId: 't2',
|
|
217
|
+
taskTitle: 'Add feature',
|
|
218
|
+
kind: 'clean' as const,
|
|
219
|
+
fixIterations: 0,
|
|
220
|
+
verdict: 'DONE',
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
scopeCreep: [],
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const result = captureQualitySignals(analysis, vault, brain, 'plan-1');
|
|
227
|
+
|
|
228
|
+
expect(brain.recordFeedback).toHaveBeenCalledWith(
|
|
229
|
+
'quality-signal:clean:Add feature',
|
|
230
|
+
't2',
|
|
231
|
+
'accepted',
|
|
232
|
+
);
|
|
233
|
+
expect(result.feedback).toBe(1);
|
|
234
|
+
expect(result.captured).toBe(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('skips duplicate anti-patterns when vault search returns high-score match', () => {
|
|
238
|
+
vault.search.mockReturnValue([{ entry: { id: 'existing' }, score: 0.8 }]);
|
|
239
|
+
|
|
240
|
+
const analysis = {
|
|
241
|
+
antiPatterns: [
|
|
242
|
+
{
|
|
243
|
+
taskId: 't1',
|
|
244
|
+
taskTitle: 'Fix login',
|
|
245
|
+
kind: 'anti-pattern' as const,
|
|
246
|
+
fixIterations: 3,
|
|
247
|
+
verdict: 'DONE',
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
cleanTasks: [],
|
|
251
|
+
scopeCreep: [],
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const result = captureQualitySignals(analysis, vault, brain, 'plan-1');
|
|
255
|
+
|
|
256
|
+
expect(vault.add).not.toHaveBeenCalled();
|
|
257
|
+
expect(result.skipped).toBe(1);
|
|
258
|
+
expect(result.captured).toBe(0);
|
|
259
|
+
// Brain feedback still recorded even for deduplicated captures
|
|
260
|
+
expect(brain.recordFeedback).toHaveBeenCalledTimes(1);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('assigns critical severity for fixIterations > 4', () => {
|
|
264
|
+
const analysis = {
|
|
265
|
+
antiPatterns: [
|
|
266
|
+
{
|
|
267
|
+
taskId: 't1',
|
|
268
|
+
taskTitle: 'Hard bug',
|
|
269
|
+
kind: 'anti-pattern' as const,
|
|
270
|
+
fixIterations: 5,
|
|
271
|
+
verdict: 'DONE',
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
cleanTasks: [],
|
|
275
|
+
scopeCreep: [],
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
captureQualitySignals(analysis, vault, brain, 'plan-1');
|
|
279
|
+
|
|
280
|
+
const entry = vault.add.mock.calls[0][0];
|
|
281
|
+
expect(entry.severity).toBe('critical');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('handles mixed signals correctly', () => {
|
|
285
|
+
const analysis = {
|
|
286
|
+
antiPatterns: [
|
|
287
|
+
{
|
|
288
|
+
taskId: 't1',
|
|
289
|
+
taskTitle: 'Rework task',
|
|
290
|
+
kind: 'anti-pattern' as const,
|
|
291
|
+
fixIterations: 4,
|
|
292
|
+
verdict: 'DONE',
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
cleanTasks: [
|
|
296
|
+
{
|
|
297
|
+
taskId: 't2',
|
|
298
|
+
taskTitle: 'Clean task',
|
|
299
|
+
kind: 'clean' as const,
|
|
300
|
+
fixIterations: 0,
|
|
301
|
+
verdict: 'DONE',
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
scopeCreep: [],
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const result = captureQualitySignals(analysis, vault, brain, 'plan-1');
|
|
308
|
+
|
|
309
|
+
expect(result.captured).toBe(1);
|
|
310
|
+
expect(result.feedback).toBe(2); // 1 dismissed + 1 accepted
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality Signals — analyze evidence reports for rework patterns and clean execution.
|
|
3
|
+
*
|
|
4
|
+
* Extracts anti-patterns (high rework), clean tasks (first-pass success),
|
|
5
|
+
* and scope creep signals from evidence reports. Captures findings to vault
|
|
6
|
+
* and feeds brain feedback. Best-effort — never throws.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { EvidenceReport } from '../planning/evidence-collector.js';
|
|
10
|
+
import type { Plan } from '../planning/planner.js';
|
|
11
|
+
import type { Vault } from '../vault/vault.js';
|
|
12
|
+
import type { Brain } from '../brain/brain.js';
|
|
13
|
+
import type { IntelligenceEntry } from '../intelligence/types.js';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Types
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export interface QualitySignal {
|
|
20
|
+
taskId: string;
|
|
21
|
+
taskTitle: string;
|
|
22
|
+
kind: 'anti-pattern' | 'clean' | 'scope-creep';
|
|
23
|
+
fixIterations: number;
|
|
24
|
+
verdict: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface QualityAnalysis {
|
|
28
|
+
antiPatterns: QualitySignal[];
|
|
29
|
+
cleanTasks: QualitySignal[];
|
|
30
|
+
scopeCreep: QualitySignal[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Thresholds
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/** Tasks with more than this many fix iterations are flagged as anti-patterns. */
|
|
38
|
+
const REWORK_THRESHOLD = 2;
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Analysis
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Analyze an evidence report for quality signals.
|
|
46
|
+
*
|
|
47
|
+
* - fixIterations > 2 → anti-pattern (rework)
|
|
48
|
+
* - fixIterations === 0 + verdict DONE → clean (first-pass success)
|
|
49
|
+
* - unplannedChanges → scope-creep signals
|
|
50
|
+
*/
|
|
51
|
+
export function analyzeQualitySignals(
|
|
52
|
+
report: EvidenceReport,
|
|
53
|
+
_plan?: Plan | null,
|
|
54
|
+
): QualityAnalysis {
|
|
55
|
+
const antiPatterns: QualitySignal[] = [];
|
|
56
|
+
const cleanTasks: QualitySignal[] = [];
|
|
57
|
+
const scopeCreep: QualitySignal[] = [];
|
|
58
|
+
|
|
59
|
+
for (const te of report.taskEvidence) {
|
|
60
|
+
const iterations = te.fixIterations ?? 0;
|
|
61
|
+
|
|
62
|
+
if (iterations > REWORK_THRESHOLD) {
|
|
63
|
+
antiPatterns.push({
|
|
64
|
+
taskId: te.taskId,
|
|
65
|
+
taskTitle: te.taskTitle,
|
|
66
|
+
kind: 'anti-pattern',
|
|
67
|
+
fixIterations: iterations,
|
|
68
|
+
verdict: te.verdict,
|
|
69
|
+
});
|
|
70
|
+
} else if (iterations === 0 && te.verdict === 'DONE') {
|
|
71
|
+
cleanTasks.push({
|
|
72
|
+
taskId: te.taskId,
|
|
73
|
+
taskTitle: te.taskTitle,
|
|
74
|
+
kind: 'clean',
|
|
75
|
+
fixIterations: 0,
|
|
76
|
+
verdict: te.verdict,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Unplanned changes signal scope creep
|
|
82
|
+
for (const uc of report.unplannedChanges) {
|
|
83
|
+
scopeCreep.push({
|
|
84
|
+
taskId: 'unplanned',
|
|
85
|
+
taskTitle: uc.file.path,
|
|
86
|
+
kind: 'scope-creep',
|
|
87
|
+
fixIterations: 0,
|
|
88
|
+
verdict: uc.possibleReason,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { antiPatterns, cleanTasks, scopeCreep };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Capture to vault + brain
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Persist quality signals to vault (anti-patterns) and brain (feedback).
|
|
101
|
+
* Deduplicates anti-patterns via vault search before adding.
|
|
102
|
+
* Best-effort — swallows all errors.
|
|
103
|
+
*/
|
|
104
|
+
export function captureQualitySignals(
|
|
105
|
+
analysis: QualityAnalysis,
|
|
106
|
+
vault: Vault,
|
|
107
|
+
brain: Brain,
|
|
108
|
+
planId: string,
|
|
109
|
+
): { captured: number; skipped: number; feedback: number } {
|
|
110
|
+
let captured = 0;
|
|
111
|
+
let skipped = 0;
|
|
112
|
+
let feedback = 0;
|
|
113
|
+
|
|
114
|
+
// Capture anti-patterns to vault (dedup first)
|
|
115
|
+
for (const ap of analysis.antiPatterns) {
|
|
116
|
+
try {
|
|
117
|
+
const query = `rework fix-trail ${ap.taskTitle}`;
|
|
118
|
+
const existing = vault.search(query, { type: 'anti-pattern', limit: 3 });
|
|
119
|
+
const isDuplicate = existing.some((r) => r.score > 0.7);
|
|
120
|
+
|
|
121
|
+
if (isDuplicate) {
|
|
122
|
+
skipped++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const severity = ap.fixIterations > 4 ? 'critical' : 'warning';
|
|
127
|
+
const entry: IntelligenceEntry = {
|
|
128
|
+
id: `qs-ap-${planId}-${ap.taskId}-${Date.now()}`,
|
|
129
|
+
type: 'anti-pattern',
|
|
130
|
+
domain: 'engineering',
|
|
131
|
+
title: `Rework detected: ${ap.taskTitle}`,
|
|
132
|
+
severity,
|
|
133
|
+
description:
|
|
134
|
+
`Task "${ap.taskTitle}" required ${ap.fixIterations} fix iterations ` +
|
|
135
|
+
`(threshold: ${REWORK_THRESHOLD}). Investigate root cause — ` +
|
|
136
|
+
`unclear requirements, missing tests, or incomplete understanding.`,
|
|
137
|
+
tags: ['rework', 'fix-trail', 'auto-captured'],
|
|
138
|
+
origin: 'agent',
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
vault.add(entry);
|
|
142
|
+
captured++;
|
|
143
|
+
} catch {
|
|
144
|
+
// Best-effort — skip this signal
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Record negative brain feedback for rework tasks
|
|
149
|
+
for (const ap of analysis.antiPatterns) {
|
|
150
|
+
try {
|
|
151
|
+
brain.recordFeedback(`quality-signal:rework:${ap.taskTitle}`, ap.taskId, 'dismissed');
|
|
152
|
+
feedback++;
|
|
153
|
+
} catch {
|
|
154
|
+
// Best-effort
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Record positive brain feedback for clean tasks
|
|
159
|
+
for (const ct of analysis.cleanTasks) {
|
|
160
|
+
try {
|
|
161
|
+
brain.recordFeedback(`quality-signal:clean:${ct.taskTitle}`, ct.taskId, 'accepted');
|
|
162
|
+
feedback++;
|
|
163
|
+
} catch {
|
|
164
|
+
// Best-effort
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { captured, skipped, feedback };
|
|
169
|
+
}
|
|
@@ -92,7 +92,7 @@ function walkDir(rootDir: string, currentDir: string, inventory: SkillInventoryI
|
|
|
92
92
|
|
|
93
93
|
if (!stat.isFile()) continue;
|
|
94
94
|
|
|
95
|
-
const relPath = relative(rootDir, fullPath);
|
|
95
|
+
const relPath = relative(rootDir, fullPath).replaceAll('\\', '/');
|
|
96
96
|
const ext = extname(name).toLowerCase();
|
|
97
97
|
const kind = classifyFile(name, ext);
|
|
98
98
|
|
|
@@ -128,6 +128,46 @@ describe('vault-markdown-sync', () => {
|
|
|
128
128
|
const filePath = join(deepDir, 'vault', 'architecture', 'deep-entry.md');
|
|
129
129
|
expect(existsSync(filePath)).toBe(true);
|
|
130
130
|
});
|
|
131
|
+
|
|
132
|
+
it('should skip rewrite when content hash matches (dedup)', async () => {
|
|
133
|
+
const entry = makeEntry({ domain: 'design', title: 'Stable Token' });
|
|
134
|
+
|
|
135
|
+
// First write
|
|
136
|
+
const first = await syncEntryToMarkdown(entry, tmpDir);
|
|
137
|
+
expect(first.written).toBe(true);
|
|
138
|
+
|
|
139
|
+
const filePath = join(tmpDir, 'vault', 'design', 'stable-token.md');
|
|
140
|
+
const mtimeBefore = readFileSync(filePath, 'utf-8');
|
|
141
|
+
|
|
142
|
+
// Second write with same content — should skip
|
|
143
|
+
const second = await syncEntryToMarkdown(entry, tmpDir);
|
|
144
|
+
expect(second.written).toBe(false);
|
|
145
|
+
|
|
146
|
+
// File content should be identical (not rewritten)
|
|
147
|
+
const mtimeAfter = readFileSync(filePath, 'utf-8');
|
|
148
|
+
expect(mtimeAfter).toBe(mtimeBefore);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should rewrite when content changes', async () => {
|
|
152
|
+
const entry = makeEntry({ domain: 'design', title: 'Changing Token' });
|
|
153
|
+
const first = await syncEntryToMarkdown(entry, tmpDir);
|
|
154
|
+
expect(first.written).toBe(true);
|
|
155
|
+
|
|
156
|
+
// Modify the entry
|
|
157
|
+
entry.description = 'Updated description that changes the hash.';
|
|
158
|
+
const second = await syncEntryToMarkdown(entry, tmpDir);
|
|
159
|
+
expect(second.written).toBe(true);
|
|
160
|
+
|
|
161
|
+
const filePath = join(tmpDir, 'vault', 'design', 'changing-token.md');
|
|
162
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
163
|
+
expect(content).toContain('Updated description');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should return written:false for empty slug', async () => {
|
|
167
|
+
const entry = makeEntry({ title: '!!!' }); // slugifies to empty
|
|
168
|
+
const result = await syncEntryToMarkdown(entry, tmpDir);
|
|
169
|
+
expect(result.written).toBe(false);
|
|
170
|
+
});
|
|
131
171
|
});
|
|
132
172
|
|
|
133
173
|
// ── syncAllToMarkdown ────────────────────────────────────────────
|
|
@@ -76,21 +76,34 @@ export function entryToMarkdown(entry: IntelligenceEntry): string {
|
|
|
76
76
|
|
|
77
77
|
// ─── Sync ───────────────────────────────────────────────────────────
|
|
78
78
|
|
|
79
|
-
/** Write a single entry as a markdown file to knowledge/vault/{domain}/{slug}.md
|
|
79
|
+
/** Write a single entry as a markdown file to knowledge/vault/{domain}/{slug}.md.
|
|
80
|
+
* Skips the write if the file already exists with a matching content hash (dedup). */
|
|
80
81
|
export async function syncEntryToMarkdown(
|
|
81
82
|
entry: IntelligenceEntry,
|
|
82
83
|
knowledgeDir: string,
|
|
83
|
-
): Promise<
|
|
84
|
+
): Promise<{ written: boolean }> {
|
|
84
85
|
const domain = entry.domain || '_general';
|
|
85
86
|
const slug = titleToSlug(entry.title);
|
|
86
|
-
if (!slug) return;
|
|
87
|
+
if (!slug) return { written: false };
|
|
87
88
|
|
|
88
89
|
const dir = join(knowledgeDir, 'vault', domain);
|
|
89
90
|
mkdirSync(dir, { recursive: true });
|
|
90
91
|
|
|
91
92
|
const filePath = join(dir, `${slug}.md`);
|
|
93
|
+
|
|
94
|
+
// Content-hash dedup: skip rewrite when file content hasn't changed
|
|
95
|
+
const contentHash = computeContentHash(entry);
|
|
96
|
+
if (existsSync(filePath)) {
|
|
97
|
+
const existing = readFileSync(filePath, 'utf-8');
|
|
98
|
+
const hashMatch = existing.match(/^content_hash:\s*"([^"]+)"/m);
|
|
99
|
+
if (hashMatch && hashMatch[1] === contentHash) {
|
|
100
|
+
return { written: false };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
const content = entryToMarkdown(entry);
|
|
93
105
|
writeFileSync(filePath, content, 'utf-8');
|
|
106
|
+
return { written: true };
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
/** Sync all vault entries to markdown, skipping entries whose content hash matches. */
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow overrides — barrel export.
|
|
3
|
+
*/
|
|
4
|
+
export {
|
|
5
|
+
loadAgentWorkflows,
|
|
6
|
+
getWorkflowForIntent,
|
|
7
|
+
WORKFLOW_TO_INTENT,
|
|
8
|
+
WorkflowGateSchema,
|
|
9
|
+
WorkflowOverrideSchema,
|
|
10
|
+
} from './workflow-loader.js';
|
|
11
|
+
|
|
12
|
+
export type { WorkflowGate, WorkflowOverride } from './workflow-loader.js';
|