smart-context-mcp 1.4.0 → 1.6.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/README.md +129 -1
- package/package.json +12 -3
- package/scripts/doctor-state.js +89 -0
- package/scripts/init-clients.js +48 -7
- package/scripts/report-adoption-metrics.js +228 -0
- package/scripts/report-metrics.js +12 -1
- package/scripts/report-workflow-metrics.js +35 -2
- package/scripts/task-runner.js +238 -0
- package/src/analytics/product-quality.js +206 -0
- package/src/client-contract.js +153 -0
- package/src/context-patterns.js +11 -0
- package/src/hooks/claude-hooks.js +48 -49
- package/src/metrics.js +19 -16
- package/src/orchestration/headless-wrapper.js +20 -26
- package/src/repo-safety.js +21 -0
- package/src/server.js +14 -4
- package/src/storage/sqlite.js +241 -12
- package/src/task-runner/policy.js +354 -0
- package/src/task-runner.js +631 -0
- package/src/tools/smart-doctor.js +418 -0
- package/src/tools/smart-metrics.js +54 -30
- package/src/tools/smart-status.js +79 -7
- package/src/tools/smart-summary.js +29 -29
- package/src/tools/smart-turn.js +455 -14
- package/src/utils/mutation-safety.js +85 -0
- package/src/workflow-tracker.js +261 -39
|
@@ -116,14 +116,31 @@ const printSummary = (summary) => {
|
|
|
116
116
|
const totalRaw = summary.reduce((sum, s) => sum + s.total_raw_tokens, 0);
|
|
117
117
|
const totalCompressed = summary.reduce((sum, s) => sum + s.total_compressed_tokens, 0);
|
|
118
118
|
const totalSaved = summary.reduce((sum, s) => sum + s.total_saved_tokens, 0);
|
|
119
|
+
const totalOverhead = summary.reduce((sum, s) => sum + (s.total_overhead_tokens || 0), 0);
|
|
120
|
+
const totalNetSaved = summary.reduce((sum, s) => sum + (s.total_net_saved_tokens || 0), 0);
|
|
121
|
+
const totalNetCoverage = summary.reduce(
|
|
122
|
+
(sum, s) => sum + (s.netMetricsCoverage?.coveredWorkflows ?? s.net_metrics_count ?? 0),
|
|
123
|
+
0,
|
|
124
|
+
);
|
|
119
125
|
const totalBaseline = summary.reduce((sum, s) => sum + s.total_baseline_tokens, 0);
|
|
126
|
+
const totalSavedPct = totalRaw > 0 ? ((totalSaved / totalRaw) * 100).toFixed(2) : '0.00';
|
|
127
|
+
const totalNetSavedPct = totalRaw > 0 ? ((totalNetSaved / totalRaw) * 100).toFixed(2) : '0.00';
|
|
128
|
+
const baselineSavingsPct = totalBaseline > 0
|
|
129
|
+
? (((totalBaseline - totalCompressed) / totalBaseline) * 100).toFixed(2)
|
|
130
|
+
: '0.00';
|
|
120
131
|
|
|
121
132
|
console.log(`Total Workflows: ${formatNumber(totalWorkflows)}`);
|
|
122
133
|
console.log(`Total Raw Tokens: ${formatNumber(totalRaw)}`);
|
|
123
134
|
console.log(`Total Compressed Tokens: ${formatNumber(totalCompressed)}`);
|
|
124
|
-
console.log(`Total Saved Tokens: ${formatNumber(totalSaved)} (${
|
|
135
|
+
console.log(`Total Saved Tokens: ${formatNumber(totalSaved)} (${totalSavedPct}%)`);
|
|
136
|
+
if (totalNetCoverage > 0) {
|
|
137
|
+
console.log(`Total Overhead Tokens: ${formatNumber(totalOverhead)}`);
|
|
138
|
+
console.log(
|
|
139
|
+
`Total Net Saved Tokens${totalNetCoverage < totalWorkflows ? ` (${formatNumber(totalNetCoverage)}/${formatNumber(totalWorkflows)} workflows)` : ''}: ${formatNumber(totalNetSaved)} (${totalNetSavedPct}%)`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
125
142
|
console.log(`Total Baseline Tokens: ${formatNumber(totalBaseline)}`);
|
|
126
|
-
console.log(`Savings vs Baseline: ${formatNumber(totalBaseline - totalCompressed)} (${
|
|
143
|
+
console.log(`Savings vs Baseline: ${formatNumber(totalBaseline - totalCompressed)} (${baselineSavingsPct}%)`);
|
|
127
144
|
console.log('');
|
|
128
145
|
console.log('By Workflow Type:');
|
|
129
146
|
console.log('─'.repeat(120));
|
|
@@ -169,6 +186,13 @@ const printSummary = (summary) => {
|
|
|
169
186
|
console.log(` Total Raw Tokens: ${formatNumber(s.total_raw_tokens)}`);
|
|
170
187
|
console.log(` Total Compressed Tokens: ${formatNumber(s.total_compressed_tokens)}`);
|
|
171
188
|
console.log(` Total Saved Tokens: ${formatNumber(s.total_saved_tokens)} (${s.avgSavingsPct}%)`);
|
|
189
|
+
const coveredWorkflows = s.netMetricsCoverage?.coveredWorkflows ?? s.net_metrics_count ?? 0;
|
|
190
|
+
if (coveredWorkflows > 0) {
|
|
191
|
+
console.log(` Total Overhead Tokens: ${formatNumber(s.total_overhead_tokens || 0)}`);
|
|
192
|
+
console.log(
|
|
193
|
+
` Total Net Saved Tokens${coveredWorkflows < s.count ? ` (${formatNumber(coveredWorkflows)}/${formatNumber(s.count)} workflows)` : ''}: ${formatNumber(s.total_net_saved_tokens || 0)}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
172
196
|
console.log(` Baseline Tokens: ${formatNumber(s.total_baseline_tokens)}`);
|
|
173
197
|
console.log(` Savings vs Baseline: ${formatNumber(s.total_baseline_tokens - s.total_compressed_tokens)} (${s.avgVsBaselinePct}%)`);
|
|
174
198
|
console.log('');
|
|
@@ -205,6 +229,15 @@ const printWorkflows = (workflows) => {
|
|
|
205
229
|
console.log(` Raw Tokens: ${formatNumber(w.raw_tokens)}`);
|
|
206
230
|
console.log(` Compressed Tokens: ${formatNumber(w.compressed_tokens)}`);
|
|
207
231
|
console.log(` Saved Tokens: ${formatNumber(w.saved_tokens)} (${w.savings_pct}%)`);
|
|
232
|
+
if (w.overheadTokens !== undefined) {
|
|
233
|
+
console.log(` Overhead Tokens: ${formatNumber(w.overheadTokens)}`);
|
|
234
|
+
}
|
|
235
|
+
if (w.netSavedTokens !== undefined) {
|
|
236
|
+
console.log(` Net Saved Tokens: ${formatNumber(w.netSavedTokens)}`);
|
|
237
|
+
}
|
|
238
|
+
if (w.netMetricsCoverage?.available === false) {
|
|
239
|
+
console.log(' Net Metrics Coverage: unavailable');
|
|
240
|
+
}
|
|
208
241
|
console.log(` Baseline Tokens: ${formatNumber(w.baseline_tokens)}`);
|
|
209
242
|
console.log(` Savings vs Baseline: ${formatNumber(w.baseline_tokens - w.compressed_tokens)} (${w.vs_baseline_pct}%)`);
|
|
210
243
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runTaskRunner } from '../src/task-runner.js';
|
|
3
|
+
|
|
4
|
+
const requireValue = (argv, index, flag) => {
|
|
5
|
+
const value = argv[index + 1];
|
|
6
|
+
if (!value || value === '--') {
|
|
7
|
+
throw new Error(`Missing value for ${flag}`);
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const parseListValue = (raw) =>
|
|
13
|
+
raw
|
|
14
|
+
.split(',')
|
|
15
|
+
.map((value) => value.trim())
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
|
|
18
|
+
const parseArgs = (argv) => {
|
|
19
|
+
const subcommand = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'task';
|
|
20
|
+
const rest = argv[0] && !argv[0].startsWith('--') ? argv.slice(1) : argv;
|
|
21
|
+
const options = {
|
|
22
|
+
commandName: subcommand,
|
|
23
|
+
client: 'generic',
|
|
24
|
+
prompt: '',
|
|
25
|
+
sessionId: undefined,
|
|
26
|
+
event: undefined,
|
|
27
|
+
stdinPrompt: false,
|
|
28
|
+
dryRun: false,
|
|
29
|
+
json: false,
|
|
30
|
+
streamOutput: true,
|
|
31
|
+
allowDegraded: false,
|
|
32
|
+
verifyIntegrity: true,
|
|
33
|
+
format: 'compact',
|
|
34
|
+
maxItems: 10,
|
|
35
|
+
cleanupMode: 'compact',
|
|
36
|
+
apply: false,
|
|
37
|
+
retentionDays: 30,
|
|
38
|
+
keepLatestEventsPerSession: 20,
|
|
39
|
+
keepLatestMetrics: 1000,
|
|
40
|
+
vacuum: false,
|
|
41
|
+
command: '',
|
|
42
|
+
args: [],
|
|
43
|
+
update: {},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const listFields = new Map([
|
|
47
|
+
['--completed', 'completed'],
|
|
48
|
+
['--decisions', 'decisions'],
|
|
49
|
+
['--blockers', 'blockers'],
|
|
50
|
+
['--touched-files', 'touchedFiles'],
|
|
51
|
+
['--pinned-context', 'pinnedContext'],
|
|
52
|
+
['--unresolved-questions', 'unresolvedQuestions'],
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
let commandIndex = rest.indexOf('--');
|
|
56
|
+
const head = commandIndex === -1 ? rest : rest.slice(0, commandIndex);
|
|
57
|
+
|
|
58
|
+
for (let index = 0; index < head.length; index += 1) {
|
|
59
|
+
const token = head[index];
|
|
60
|
+
|
|
61
|
+
if (token === '--client') {
|
|
62
|
+
options.client = requireValue(head, index, '--client');
|
|
63
|
+
index += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (token === '--prompt') {
|
|
68
|
+
options.prompt = requireValue(head, index, '--prompt');
|
|
69
|
+
index += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (token === '--session-id') {
|
|
74
|
+
options.sessionId = requireValue(head, index, '--session-id');
|
|
75
|
+
index += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (token === '--event') {
|
|
80
|
+
options.event = requireValue(head, index, '--event');
|
|
81
|
+
index += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (token === '--stdin-prompt') {
|
|
86
|
+
options.stdinPrompt = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (token === '--dry-run') {
|
|
91
|
+
options.dryRun = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (token === '--json') {
|
|
96
|
+
options.json = true;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (token === '--quiet') {
|
|
101
|
+
options.streamOutput = false;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (token === '--allow-degraded') {
|
|
106
|
+
options.allowDegraded = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (token === '--no-integrity') {
|
|
111
|
+
options.verifyIntegrity = false;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (token === '--format') {
|
|
116
|
+
options.format = requireValue(head, index, '--format');
|
|
117
|
+
index += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (token === '--max-items') {
|
|
122
|
+
options.maxItems = Number(requireValue(head, index, '--max-items'));
|
|
123
|
+
index += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (token === '--cleanup-mode') {
|
|
128
|
+
options.cleanupMode = requireValue(head, index, '--cleanup-mode');
|
|
129
|
+
index += 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (token === '--apply') {
|
|
134
|
+
options.apply = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (token === '--retention-days') {
|
|
139
|
+
options.retentionDays = Number(requireValue(head, index, '--retention-days'));
|
|
140
|
+
index += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (token === '--keep-latest-events') {
|
|
145
|
+
options.keepLatestEventsPerSession = Number(requireValue(head, index, '--keep-latest-events'));
|
|
146
|
+
index += 1;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (token === '--keep-latest-metrics') {
|
|
151
|
+
options.keepLatestMetrics = Number(requireValue(head, index, '--keep-latest-metrics'));
|
|
152
|
+
index += 1;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (token === '--vacuum') {
|
|
157
|
+
options.vacuum = true;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (token === '--goal') {
|
|
162
|
+
options.update.goal = requireValue(head, index, '--goal');
|
|
163
|
+
index += 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (token === '--status') {
|
|
168
|
+
options.update.status = requireValue(head, index, '--status');
|
|
169
|
+
index += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (token === '--current-focus') {
|
|
174
|
+
options.update.currentFocus = requireValue(head, index, '--current-focus');
|
|
175
|
+
index += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (token === '--why-blocked') {
|
|
180
|
+
options.update.whyBlocked = requireValue(head, index, '--why-blocked');
|
|
181
|
+
index += 1;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (token === '--next-step') {
|
|
186
|
+
options.update.nextStep = requireValue(head, index, '--next-step');
|
|
187
|
+
index += 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (listFields.has(token)) {
|
|
192
|
+
const field = listFields.get(token);
|
|
193
|
+
options.update[field] = parseListValue(requireValue(head, index, token));
|
|
194
|
+
index += 1;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
throw new Error(`Unknown argument: ${token}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (commandIndex !== -1) {
|
|
202
|
+
const commandParts = rest.slice(commandIndex + 1);
|
|
203
|
+
if (commandParts.length > 0) {
|
|
204
|
+
[options.command, ...options.args] = commandParts;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return options;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const main = async () => {
|
|
212
|
+
const options = parseArgs(process.argv.slice(2));
|
|
213
|
+
if (options.json) {
|
|
214
|
+
options.streamOutput = false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const result = await runTaskRunner(options);
|
|
218
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
219
|
+
|
|
220
|
+
if (result?.blocked) {
|
|
221
|
+
process.exitCode = 2;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (typeof result?.exitCode === 'number' && result.exitCode !== 0) {
|
|
226
|
+
process.exitCode = result.exitCode;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (result?.overall === 'error') {
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
main().catch((error) => {
|
|
236
|
+
console.error(error.message);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
const roundPct = (value, total) =>
|
|
2
|
+
total > 0 ? Number(((value / total) * 100).toFixed(1)) : 0;
|
|
3
|
+
|
|
4
|
+
export const PRODUCT_QUALITY_ANALYTICS_KIND = 'smart_turn_quality';
|
|
5
|
+
export const TASK_RUNNER_QUALITY_ANALYTICS_KIND = 'task_runner_quality';
|
|
6
|
+
|
|
7
|
+
const isProductQualityEntry = (entry) =>
|
|
8
|
+
entry?.tool === 'smart_turn'
|
|
9
|
+
&& entry?.metadata?.analyticsKind === PRODUCT_QUALITY_ANALYTICS_KIND;
|
|
10
|
+
|
|
11
|
+
const isTaskRunnerQualityEntry = (entry) =>
|
|
12
|
+
entry?.tool === 'task_runner'
|
|
13
|
+
&& entry?.metadata?.analyticsKind === TASK_RUNNER_QUALITY_ANALYTICS_KIND;
|
|
14
|
+
|
|
15
|
+
const analyzeTaskRunnerQuality = (entries = []) => {
|
|
16
|
+
const runnerEntries = entries.filter(isTaskRunnerQualityEntry);
|
|
17
|
+
const workflowEntries = runnerEntries.filter((entry) => entry.metadata?.isWorkflowCommand);
|
|
18
|
+
const specializedWorkflowEntries = workflowEntries.filter((entry) => entry.metadata?.specializedWorkflow);
|
|
19
|
+
const specializedExecutableEntries = specializedWorkflowEntries.filter((entry) => !entry.metadata?.blocked);
|
|
20
|
+
const blockedEntries = runnerEntries.filter((entry) => entry.metadata?.blocked);
|
|
21
|
+
const doctorEntries = runnerEntries.filter((entry) => entry.metadata?.doctorIssued);
|
|
22
|
+
const wrappedEntries = workflowEntries.filter((entry) => entry.metadata?.usedWrapper);
|
|
23
|
+
const workflowPolicyEntries = workflowEntries.filter((entry) => entry.metadata?.workflowPolicyMode);
|
|
24
|
+
const preflightEntries = specializedWorkflowEntries.filter((entry) => entry.metadata?.workflowPreflightTool);
|
|
25
|
+
const blockedWithDoctor = blockedEntries.filter((entry) => entry.metadata?.doctorIssued);
|
|
26
|
+
const checkpointEntries = runnerEntries.filter((entry) => entry.action === 'checkpoint');
|
|
27
|
+
const persistedCheckpointEntries = checkpointEntries.filter((entry) => entry.metadata?.checkpointPersisted);
|
|
28
|
+
|
|
29
|
+
const commandBreakdown = [...runnerEntries.reduce((acc, entry) => {
|
|
30
|
+
const key = entry.action ?? 'unknown';
|
|
31
|
+
const current = acc.get(key) ?? {
|
|
32
|
+
command: key,
|
|
33
|
+
count: 0,
|
|
34
|
+
blocked: 0,
|
|
35
|
+
doctorIssued: 0,
|
|
36
|
+
usedWrapper: 0,
|
|
37
|
+
preflighted: 0,
|
|
38
|
+
};
|
|
39
|
+
current.count += 1;
|
|
40
|
+
current.blocked += entry.metadata?.blocked ? 1 : 0;
|
|
41
|
+
current.doctorIssued += entry.metadata?.doctorIssued ? 1 : 0;
|
|
42
|
+
current.usedWrapper += entry.metadata?.usedWrapper ? 1 : 0;
|
|
43
|
+
current.preflighted += entry.metadata?.workflowPreflightTool ? 1 : 0;
|
|
44
|
+
acc.set(key, current);
|
|
45
|
+
return acc;
|
|
46
|
+
}, new Map()).values()].sort((a, b) => b.count - a.count || a.command.localeCompare(b.command));
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
commandsMeasured: runnerEntries.length,
|
|
50
|
+
workflowCommands: workflowEntries.length,
|
|
51
|
+
specializedWorkflowCommands: specializedWorkflowEntries.length,
|
|
52
|
+
blockedCommands: blockedEntries.length,
|
|
53
|
+
doctorCommands: doctorEntries.length,
|
|
54
|
+
dryRunCommands: runnerEntries.filter((entry) => entry.metadata?.dryRun).length,
|
|
55
|
+
commandBreakdown,
|
|
56
|
+
workflowPolicy: {
|
|
57
|
+
coveredCommands: workflowPolicyEntries.length,
|
|
58
|
+
preflightedCommands: preflightEntries.length,
|
|
59
|
+
wrapperBackedCommands: wrappedEntries.length,
|
|
60
|
+
coveragePct: roundPct(workflowPolicyEntries.length, workflowEntries.length),
|
|
61
|
+
preflightCoveragePct: roundPct(preflightEntries.length, specializedWorkflowEntries.length),
|
|
62
|
+
wrapperCoveragePct: roundPct(wrappedEntries.length, workflowEntries.length),
|
|
63
|
+
},
|
|
64
|
+
blockedState: {
|
|
65
|
+
blockedCommands: blockedEntries.length,
|
|
66
|
+
blockedWithDoctor: blockedWithDoctor.length,
|
|
67
|
+
doctorCoveragePct: roundPct(blockedWithDoctor.length, blockedEntries.length),
|
|
68
|
+
},
|
|
69
|
+
checkpointing: {
|
|
70
|
+
commandsMeasured: checkpointEntries.length,
|
|
71
|
+
persistedCommands: persistedCheckpointEntries.length,
|
|
72
|
+
persistenceRatePct: roundPct(persistedCheckpointEntries.length, checkpointEntries.length),
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const analyzeProductQuality = (entries = []) => {
|
|
78
|
+
const qualityEntries = entries.filter(isProductQualityEntry);
|
|
79
|
+
const startEntries = qualityEntries.filter((entry) => entry.metadata?.phase === 'start');
|
|
80
|
+
const endEntries = qualityEntries.filter((entry) => entry.metadata?.phase === 'end');
|
|
81
|
+
|
|
82
|
+
const alignedStarts = startEntries.filter((entry) => entry.metadata?.continuityState === 'aligned').length;
|
|
83
|
+
const reusableStarts = startEntries.filter((entry) => entry.metadata?.shouldReuseContext).length;
|
|
84
|
+
const isolatedStarts = startEntries.filter((entry) => entry.metadata?.isolatedSession).length;
|
|
85
|
+
const ambiguousStarts = startEntries.filter((entry) => entry.metadata?.continuityState === 'ambiguous_resume').length;
|
|
86
|
+
const coldStarts = startEntries.filter((entry) => entry.metadata?.continuityState === 'cold_start').length;
|
|
87
|
+
|
|
88
|
+
const blockedTurns = qualityEntries.filter((entry) => entry.metadata?.mutationBlocked).length;
|
|
89
|
+
const blockedWithActions = qualityEntries.filter((entry) =>
|
|
90
|
+
entry.metadata?.mutationBlocked && Number(entry.metadata?.recommendedActionsCount ?? 0) > 0
|
|
91
|
+
).length;
|
|
92
|
+
|
|
93
|
+
const refreshedStarts = startEntries.filter((entry) => entry.metadata?.refreshedContext).length;
|
|
94
|
+
const refreshedWithTopFiles = startEntries.filter((entry) => Number(entry.metadata?.refreshedTopFiles ?? 0) > 0).length;
|
|
95
|
+
const indexRefreshedStarts = startEntries.filter((entry) => entry.metadata?.indexRefreshed).length;
|
|
96
|
+
|
|
97
|
+
const persistedEnds = endEntries.filter((entry) => entry.metadata?.checkpointPersisted).length;
|
|
98
|
+
const skippedEnds = endEntries.filter((entry) => entry.metadata?.checkpointSkipped).length;
|
|
99
|
+
const blockedEnds = endEntries.filter((entry) => entry.metadata?.mutationBlocked).length;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
turnsMeasured: qualityEntries.length,
|
|
103
|
+
startsMeasured: startEntries.length,
|
|
104
|
+
endsMeasured: endEntries.length,
|
|
105
|
+
continuityRecovery: {
|
|
106
|
+
startsMeasured: startEntries.length,
|
|
107
|
+
alignedStarts,
|
|
108
|
+
reusableStarts,
|
|
109
|
+
isolatedStarts,
|
|
110
|
+
ambiguousStarts,
|
|
111
|
+
coldStarts,
|
|
112
|
+
alignmentRatePct: roundPct(alignedStarts, startEntries.length),
|
|
113
|
+
reusableRatePct: roundPct(reusableStarts, startEntries.length),
|
|
114
|
+
},
|
|
115
|
+
blockedState: {
|
|
116
|
+
turnsBlocked: blockedTurns,
|
|
117
|
+
blockedStarts: qualityEntries.filter((entry) => entry.metadata?.phase === 'start' && entry.metadata?.mutationBlocked).length,
|
|
118
|
+
blockedEnds,
|
|
119
|
+
blockedWithRecommendedActions: blockedWithActions,
|
|
120
|
+
remediationCoveragePct: roundPct(blockedWithActions, blockedTurns),
|
|
121
|
+
},
|
|
122
|
+
contextRefresh: {
|
|
123
|
+
refreshedStarts,
|
|
124
|
+
refreshedWithTopFiles,
|
|
125
|
+
indexRefreshedStarts,
|
|
126
|
+
topFileSignalRatePct: roundPct(refreshedWithTopFiles, refreshedStarts),
|
|
127
|
+
},
|
|
128
|
+
checkpointing: {
|
|
129
|
+
endsMeasured: endEntries.length,
|
|
130
|
+
persistedEnds,
|
|
131
|
+
skippedEnds,
|
|
132
|
+
blockedEnds,
|
|
133
|
+
persistenceRatePct: roundPct(persistedEnds, endEntries.length),
|
|
134
|
+
},
|
|
135
|
+
taskRunner: analyzeTaskRunnerQuality(entries),
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const formatProductQualityReport = (stats) => {
|
|
140
|
+
const hasSmartTurn = Number(stats?.turnsMeasured ?? 0) > 0;
|
|
141
|
+
const hasTaskRunner = Number(stats?.taskRunner?.commandsMeasured ?? 0) > 0;
|
|
142
|
+
|
|
143
|
+
if (!stats || (!hasSmartTurn && !hasTaskRunner)) {
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const lines = [];
|
|
148
|
+
lines.push('');
|
|
149
|
+
lines.push('Product Quality Signals');
|
|
150
|
+
lines.push('');
|
|
151
|
+
|
|
152
|
+
if (hasSmartTurn) {
|
|
153
|
+
lines.push('smart_turn orchestration:');
|
|
154
|
+
lines.push(`Turns measured: ${stats.turnsMeasured}`);
|
|
155
|
+
lines.push(`Start turns: ${stats.startsMeasured}`);
|
|
156
|
+
lines.push(`End turns: ${stats.endsMeasured}`);
|
|
157
|
+
lines.push('');
|
|
158
|
+
lines.push('Continuity Recovery:');
|
|
159
|
+
lines.push(`Aligned starts: ${stats.continuityRecovery.alignedStarts}/${stats.continuityRecovery.startsMeasured} (${stats.continuityRecovery.alignmentRatePct}%)`);
|
|
160
|
+
lines.push(`Reusable starts: ${stats.continuityRecovery.reusableStarts}/${stats.continuityRecovery.startsMeasured} (${stats.continuityRecovery.reusableRatePct}%)`);
|
|
161
|
+
lines.push(`Isolated starts: ${stats.continuityRecovery.isolatedStarts}`);
|
|
162
|
+
lines.push(`Ambiguous resumes: ${stats.continuityRecovery.ambiguousStarts}`);
|
|
163
|
+
lines.push('');
|
|
164
|
+
lines.push('Blocked-State Handling:');
|
|
165
|
+
lines.push(`Blocked turns: ${stats.blockedState.turnsBlocked}`);
|
|
166
|
+
lines.push(`With remediation: ${stats.blockedState.blockedWithRecommendedActions}/${stats.blockedState.turnsBlocked} (${stats.blockedState.remediationCoveragePct}%)`);
|
|
167
|
+
lines.push('');
|
|
168
|
+
lines.push('Context Refresh Signals:');
|
|
169
|
+
lines.push(`Refreshed starts: ${stats.contextRefresh.refreshedStarts}`);
|
|
170
|
+
lines.push(`With top-file signal: ${stats.contextRefresh.refreshedWithTopFiles}/${stats.contextRefresh.refreshedStarts} (${stats.contextRefresh.topFileSignalRatePct}%)`);
|
|
171
|
+
lines.push(`Index refreshes: ${stats.contextRefresh.indexRefreshedStarts}`);
|
|
172
|
+
lines.push('');
|
|
173
|
+
lines.push('Checkpointing:');
|
|
174
|
+
lines.push(`Persisted ends: ${stats.checkpointing.persistedEnds}/${stats.checkpointing.endsMeasured} (${stats.checkpointing.persistenceRatePct}%)`);
|
|
175
|
+
lines.push(`Skipped ends: ${stats.checkpointing.skippedEnds}`);
|
|
176
|
+
lines.push(`Blocked ends: ${stats.checkpointing.blockedEnds}`);
|
|
177
|
+
lines.push('');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (hasTaskRunner) {
|
|
181
|
+
lines.push('task_runner workflows:');
|
|
182
|
+
lines.push(`Commands measured: ${stats.taskRunner.commandsMeasured}`);
|
|
183
|
+
lines.push(`Workflow commands: ${stats.taskRunner.workflowCommands}`);
|
|
184
|
+
lines.push(`Specialized commands: ${stats.taskRunner.specializedWorkflowCommands}`);
|
|
185
|
+
lines.push(`Blocked commands: ${stats.taskRunner.blockedCommands}`);
|
|
186
|
+
lines.push(`Doctor commands: ${stats.taskRunner.doctorCommands}`);
|
|
187
|
+
lines.push('');
|
|
188
|
+
lines.push('Workflow Policy Coverage:');
|
|
189
|
+
lines.push(`Policy-backed: ${stats.taskRunner.workflowPolicy.coveredCommands}/${stats.taskRunner.workflowCommands} (${stats.taskRunner.workflowPolicy.coveragePct}%)`);
|
|
190
|
+
lines.push(`Preflighted: ${stats.taskRunner.workflowPolicy.preflightedCommands}/${stats.taskRunner.specializedWorkflowCommands} (${stats.taskRunner.workflowPolicy.preflightCoveragePct}%)`);
|
|
191
|
+
lines.push(`Wrapper-backed: ${stats.taskRunner.workflowPolicy.wrapperBackedCommands}/${stats.taskRunner.workflowCommands} (${stats.taskRunner.workflowPolicy.wrapperCoveragePct}%)`);
|
|
192
|
+
lines.push('');
|
|
193
|
+
lines.push('Blocked-State Routing:');
|
|
194
|
+
lines.push(`Blocked with doctor: ${stats.taskRunner.blockedState.blockedWithDoctor}/${stats.taskRunner.blockedState.blockedCommands} (${stats.taskRunner.blockedState.doctorCoveragePct}%)`);
|
|
195
|
+
lines.push('');
|
|
196
|
+
lines.push('Checkpoint Commands:');
|
|
197
|
+
lines.push(`Persisted checkpoints: ${stats.taskRunner.checkpointing.persistedCommands}/${stats.taskRunner.checkpointing.commandsMeasured} (${stats.taskRunner.checkpointing.persistenceRatePct}%)`);
|
|
198
|
+
lines.push('');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
lines.push('Notes:');
|
|
202
|
+
lines.push('- These are measured orchestration signals, not direct answer-quality scores.');
|
|
203
|
+
lines.push('- Context refresh usefulness is proxied by whether refreshed turns surfaced top-file signals.');
|
|
204
|
+
lines.push('');
|
|
205
|
+
return lines.join('\n');
|
|
206
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const normalizeWhitespace = (value) => String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
2
|
+
|
|
3
|
+
const truncate = (value, maxLength = 120) => {
|
|
4
|
+
const normalized = normalizeWhitespace(value);
|
|
5
|
+
if (normalized.length <= maxLength) {
|
|
6
|
+
return normalized;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (maxLength <= 3) {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return `${normalized.slice(0, maxLength - 3)}...`;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const CLIENT_CONTRACT_RULE_LINES = [
|
|
17
|
+
'Treat smart_turn as the task entry point for non-trivial work.',
|
|
18
|
+
'If smart_turn returns mutationSafety.blocked = true, stop write-heavy work, surface blockedBy, and follow recommendedActions before retrying persisted steps.',
|
|
19
|
+
'If smart_turn or smart_doctor reports storageHealth.issue !== "ok", pause persisted context writes and remediate local state before continuing.',
|
|
20
|
+
'Use smart_doctor when repo safety or SQLite health is unhealthy or unclear.',
|
|
21
|
+
'Use workflow, continuity, recommendedPath, mutationSafety, and storageHealth as the current operational state for the task.',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export const buildMutationSafetyActionLines = (
|
|
25
|
+
mutationSafety,
|
|
26
|
+
{
|
|
27
|
+
prefix = 'fix',
|
|
28
|
+
maxLength = 110,
|
|
29
|
+
maxItems = 2,
|
|
30
|
+
} = {},
|
|
31
|
+
) =>
|
|
32
|
+
(mutationSafety?.recommendedActions ?? [])
|
|
33
|
+
.slice(0, maxItems)
|
|
34
|
+
.map((action) => `${prefix}: ${truncate(action, maxLength)}`);
|
|
35
|
+
|
|
36
|
+
export const buildRecommendedPathLines = (
|
|
37
|
+
recommendedPath,
|
|
38
|
+
{
|
|
39
|
+
includePath = true,
|
|
40
|
+
maxLength = 110,
|
|
41
|
+
nextToolsLabel = 'next tools',
|
|
42
|
+
pathLabel = 'path',
|
|
43
|
+
} = {},
|
|
44
|
+
) => {
|
|
45
|
+
if (!recommendedPath) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const lines = [];
|
|
50
|
+
|
|
51
|
+
if (Array.isArray(recommendedPath.nextTools) && recommendedPath.nextTools.length > 0) {
|
|
52
|
+
lines.push(`${nextToolsLabel}: ${recommendedPath.nextTools.slice(0, 3).join(' -> ')}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (includePath && recommendedPath.steps?.[0]?.instruction) {
|
|
56
|
+
lines.push(`${pathLabel}: ${truncate(recommendedPath.steps[0].instruction, maxLength)}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return lines;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const buildOperationalContextLines = (
|
|
63
|
+
result,
|
|
64
|
+
{
|
|
65
|
+
sessionStart = false,
|
|
66
|
+
maxLineLength = 110,
|
|
67
|
+
maxLines = 7,
|
|
68
|
+
maxChars = 420,
|
|
69
|
+
} = {},
|
|
70
|
+
) => {
|
|
71
|
+
const lines = [];
|
|
72
|
+
const repoSafety = result?.repoSafety;
|
|
73
|
+
const mutationSafety = result?.mutationSafety;
|
|
74
|
+
const summary = result?.summary;
|
|
75
|
+
const continuityState = result?.continuity?.state;
|
|
76
|
+
const storageIssue = result?.storageHealth?.issue;
|
|
77
|
+
|
|
78
|
+
if (result?.found && summary) {
|
|
79
|
+
const label = sessionStart ? 'resume' : continuityState ?? 'resume';
|
|
80
|
+
lines.push(`devctx ${label}: session ${result.sessionId}`);
|
|
81
|
+
|
|
82
|
+
if (summary.goal) {
|
|
83
|
+
lines.push(`goal: ${truncate(summary.goal, maxLineLength)}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (summary.currentFocus) {
|
|
87
|
+
lines.push(`focus: ${truncate(summary.currentFocus, maxLineLength)}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!mutationSafety?.blocked) {
|
|
91
|
+
lines.push(...buildRecommendedPathLines(result?.recommendedPath, {
|
|
92
|
+
includePath: true,
|
|
93
|
+
maxLength: maxLineLength,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (summary.nextStep) {
|
|
98
|
+
lines.push(`next: ${truncate(summary.nextStep, maxLineLength)}`);
|
|
99
|
+
}
|
|
100
|
+
} else if (continuityState === 'ambiguous_resume') {
|
|
101
|
+
lines.push('devctx: multiple persisted sessions matched this prompt.');
|
|
102
|
+
if (result?.recommendedSessionId) {
|
|
103
|
+
lines.push(`recommended session: ${result.recommendedSessionId}`);
|
|
104
|
+
}
|
|
105
|
+
} else if (result?.autoCreated && summary?.goal) {
|
|
106
|
+
lines.push(`devctx new task session: ${truncate(summary.goal, maxLineLength)}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (result?.continuity?.reason) {
|
|
110
|
+
lines.push(`context status: ${truncate(result.continuity.reason, maxLineLength)}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (mutationSafety?.blocked) {
|
|
114
|
+
const reasons = mutationSafety.blockedBy?.join(' and ') || 'blocked';
|
|
115
|
+
lines.push(`repo safety: ${mutationSafety.stateDbPath} is ${reasons}; context writes are blocked.`);
|
|
116
|
+
lines.push(...buildRecommendedPathLines(result?.recommendedPath, {
|
|
117
|
+
includePath: false,
|
|
118
|
+
maxLength: maxLineLength,
|
|
119
|
+
}));
|
|
120
|
+
lines.push(...buildMutationSafetyActionLines(mutationSafety, {
|
|
121
|
+
prefix: 'fix',
|
|
122
|
+
maxLength: maxLineLength,
|
|
123
|
+
}));
|
|
124
|
+
} else if (repoSafety?.isTracked || repoSafety?.isStaged) {
|
|
125
|
+
const reasons = [];
|
|
126
|
+
if (repoSafety.isTracked) {
|
|
127
|
+
reasons.push('tracked');
|
|
128
|
+
}
|
|
129
|
+
if (repoSafety.isStaged) {
|
|
130
|
+
reasons.push('staged');
|
|
131
|
+
}
|
|
132
|
+
lines.push(`repo safety: .devctx/state.sqlite is ${reasons.join(' and ')}; context writes are blocked.`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (storageIssue && storageIssue !== 'ok') {
|
|
136
|
+
lines.push(`storage health: ${storageIssue}`);
|
|
137
|
+
lines.push('doctor: run smart_doctor before retrying persisted context writes.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (result?.refreshedContext?.indexRefreshed) {
|
|
141
|
+
lines.push('context refresh: project index was refreshed for this prompt.');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (result?.refreshedContext?.topFiles?.length > 0) {
|
|
145
|
+
lines.push(`files: ${result.refreshedContext.topFiles.map((item) => item.file).slice(0, 2).join(', ')}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (result?.refreshedContext?.hints?.[0]) {
|
|
149
|
+
lines.push(`hint: ${truncate(result.refreshedContext.hints[0], maxLineLength)}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return lines.slice(0, maxLines).join('\n').slice(0, maxChars).trim() || null;
|
|
153
|
+
};
|
package/src/context-patterns.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getRepoMutationSafety } from './repo-safety.js';
|
|
1
2
|
import { withStateDb, withStateDbSnapshot } from './storage/sqlite.js';
|
|
2
3
|
|
|
3
4
|
const PATTERN_CONFIDENCE_THRESHOLD = 0.6;
|
|
@@ -55,6 +56,7 @@ const computeTaskSimilarity = (task1, task2) => {
|
|
|
55
56
|
|
|
56
57
|
export const recordContextAccess = async ({ task, intent, files }) => {
|
|
57
58
|
if (!task || !Array.isArray(files) || files.length === 0) return;
|
|
59
|
+
if (getRepoMutationSafety().shouldBlock) return null;
|
|
58
60
|
|
|
59
61
|
const signature = normalizeTaskSignature(task);
|
|
60
62
|
if (!signature) return;
|
|
@@ -174,6 +176,15 @@ export const predictContextFiles = async ({ task, intent, maxFiles = MAX_PREDICT
|
|
|
174
176
|
};
|
|
175
177
|
|
|
176
178
|
export const cleanupStalePatterns = async ({ retentionDays = PATTERN_DECAY_DAYS } = {}) => {
|
|
179
|
+
if (getRepoMutationSafety().shouldBlock) {
|
|
180
|
+
return {
|
|
181
|
+
action: 'cleanup_patterns',
|
|
182
|
+
deletedPatterns: 0,
|
|
183
|
+
retentionDays,
|
|
184
|
+
blocked: true,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
177
188
|
return withStateDb((db) => {
|
|
178
189
|
initPatternTables(db);
|
|
179
190
|
|