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.
@@ -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)} (${((totalSaved / totalRaw) * 100).toFixed(2)}%)`);
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)} (${(((totalBaseline - totalCompressed) / totalBaseline) * 100).toFixed(2)}%)`);
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
+ };
@@ -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