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
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { persistMetrics } from '../metrics.js';
|
|
2
|
+
import { buildOperationalContextLines } from '../client-contract.js';
|
|
3
|
+
import { getRepoMutationSafety } from '../repo-safety.js';
|
|
2
4
|
import { countTokens } from '../tokenCounter.js';
|
|
3
5
|
import { smartSummary } from '../tools/smart-summary.js';
|
|
4
6
|
import { smartTurn } from '../tools/smart-turn.js';
|
|
@@ -11,7 +13,7 @@ import {
|
|
|
11
13
|
const HOOK_CLIENT = 'claude';
|
|
12
14
|
const START_MAX_TOKENS = 350;
|
|
13
15
|
const STOP_MAX_TOKENS = 300;
|
|
14
|
-
const MAX_CONTEXT_LINES =
|
|
16
|
+
const MAX_CONTEXT_LINES = 7;
|
|
15
17
|
const MAX_CONTEXT_CHARS = 420;
|
|
16
18
|
const MAX_PROMPT_PREVIEW = 160;
|
|
17
19
|
const WRITE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);
|
|
@@ -50,48 +52,12 @@ const buildHookKey = ({ sessionId, agentId = null }) =>
|
|
|
50
52
|
agentId ? `${HOOK_CLIENT}:subagent:${sessionId}:${agentId}` : `${HOOK_CLIENT}:main:${sessionId}`;
|
|
51
53
|
|
|
52
54
|
const buildAdditionalContext = ({ result, sessionStart = false }) => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const label = sessionStart ? 'resume' : continuityState ?? 'resume';
|
|
60
|
-
lines.push(`devctx ${label}: session ${result.sessionId}`);
|
|
61
|
-
|
|
62
|
-
if (summary.goal) {
|
|
63
|
-
lines.push(`goal: ${truncate(summary.goal, 110)}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (summary.currentFocus) {
|
|
67
|
-
lines.push(`focus: ${truncate(summary.currentFocus, 110)}`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (summary.nextStep) {
|
|
71
|
-
lines.push(`next: ${truncate(summary.nextStep, 110)}`);
|
|
72
|
-
}
|
|
73
|
-
} else if (result?.continuity?.state === 'ambiguous_resume') {
|
|
74
|
-
lines.push('devctx: multiple persisted sessions matched this prompt.');
|
|
75
|
-
if (result?.recommendedSessionId) {
|
|
76
|
-
lines.push(`recommended session: ${result.recommendedSessionId}`);
|
|
77
|
-
}
|
|
78
|
-
} else if (result?.autoCreated && summary?.goal) {
|
|
79
|
-
lines.push(`devctx new task session: ${truncate(summary.goal, 110)}`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (repoSafety?.isTracked || repoSafety?.isStaged) {
|
|
83
|
-
const reasons = [];
|
|
84
|
-
if (repoSafety.isTracked) {
|
|
85
|
-
reasons.push('tracked');
|
|
86
|
-
}
|
|
87
|
-
if (repoSafety.isStaged) {
|
|
88
|
-
reasons.push('staged');
|
|
89
|
-
}
|
|
90
|
-
lines.push(`repo safety: .devctx/state.sqlite is ${reasons.join(' and ')}; context writes are blocked.`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const clipped = lines.slice(0, MAX_CONTEXT_LINES).join('\n').slice(0, MAX_CONTEXT_CHARS).trim();
|
|
94
|
-
return clipped || null;
|
|
55
|
+
return buildOperationalContextLines(result, {
|
|
56
|
+
sessionStart,
|
|
57
|
+
maxLineLength: 110,
|
|
58
|
+
maxLines: MAX_CONTEXT_LINES,
|
|
59
|
+
maxChars: MAX_CONTEXT_CHARS,
|
|
60
|
+
});
|
|
95
61
|
};
|
|
96
62
|
|
|
97
63
|
const buildHookContextResponse = (hookEventName, additionalContext) => {
|
|
@@ -107,6 +73,28 @@ const buildHookContextResponse = (hookEventName, additionalContext) => {
|
|
|
107
73
|
};
|
|
108
74
|
};
|
|
109
75
|
|
|
76
|
+
const readHookTurnState = (hookKey) =>
|
|
77
|
+
getHookTurnState({
|
|
78
|
+
hookKey,
|
|
79
|
+
readOnly: getRepoMutationSafety().shouldBlock,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const maybeSetTrackedTurnState = async ({ hookKey, state }) => {
|
|
83
|
+
if (getRepoMutationSafety().shouldBlock) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return setHookTurnState({ hookKey, state });
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const maybeDeleteTrackedTurnState = async ({ hookKey }) => {
|
|
91
|
+
if (getRepoMutationSafety().shouldBlock) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return deleteHookTurnState({ hookKey });
|
|
96
|
+
};
|
|
97
|
+
|
|
110
98
|
const recordHookMetrics = async ({
|
|
111
99
|
action,
|
|
112
100
|
sessionId,
|
|
@@ -247,7 +235,7 @@ const maybeTrackTurn = async ({
|
|
|
247
235
|
return null;
|
|
248
236
|
}
|
|
249
237
|
|
|
250
|
-
return
|
|
238
|
+
return maybeSetTrackedTurnState({
|
|
251
239
|
hookKey,
|
|
252
240
|
state: {
|
|
253
241
|
client: HOOK_CLIENT,
|
|
@@ -308,7 +296,7 @@ const handleUserPromptSubmit = async (input) => {
|
|
|
308
296
|
|
|
309
297
|
const handlePostToolUse = async (input) => {
|
|
310
298
|
const hookKey = buildHookKey({ sessionId: input.session_id });
|
|
311
|
-
const existing = await
|
|
299
|
+
const existing = await readHookTurnState(hookKey);
|
|
312
300
|
if (!existing) {
|
|
313
301
|
return null;
|
|
314
302
|
}
|
|
@@ -332,7 +320,7 @@ const handlePostToolUse = async (input) => {
|
|
|
332
320
|
updatedAt: new Date().toISOString(),
|
|
333
321
|
};
|
|
334
322
|
|
|
335
|
-
await
|
|
323
|
+
await maybeSetTrackedTurnState({ hookKey, state: nextState });
|
|
336
324
|
if (checkpoint.matched || touchedFiles.length > 0) {
|
|
337
325
|
await recordHookMetrics({
|
|
338
326
|
action: 'PostToolUse',
|
|
@@ -346,11 +334,22 @@ const handlePostToolUse = async (input) => {
|
|
|
346
334
|
|
|
347
335
|
const handleStop = async (input) => {
|
|
348
336
|
const hookKey = buildHookKey({ sessionId: input.session_id });
|
|
349
|
-
const state = await
|
|
337
|
+
const state = await readHookTurnState(hookKey);
|
|
350
338
|
if (!state) {
|
|
351
339
|
return null;
|
|
352
340
|
}
|
|
353
341
|
|
|
342
|
+
if (getRepoMutationSafety().shouldBlock) {
|
|
343
|
+
await recordHookMetrics({
|
|
344
|
+
action: 'Stop',
|
|
345
|
+
sessionId: state.projectSessionId,
|
|
346
|
+
additionalContext: '',
|
|
347
|
+
blocked: false,
|
|
348
|
+
continuityState: state.continuityState,
|
|
349
|
+
});
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
354
353
|
const enforcement = computeStopEnforcement(state, input.last_assistant_message);
|
|
355
354
|
const shouldEnforce = (state.requireCheckpoint || state.meaningfulWriteCount > 0) && enforcement.shouldBlock;
|
|
356
355
|
if (!shouldEnforce || state.checkpointed) {
|
|
@@ -361,7 +360,7 @@ const handleStop = async (input) => {
|
|
|
361
360
|
blocked: false,
|
|
362
361
|
continuityState: state.continuityState,
|
|
363
362
|
});
|
|
364
|
-
await
|
|
363
|
+
await maybeDeleteTrackedTurnState({ hookKey });
|
|
365
364
|
return null;
|
|
366
365
|
}
|
|
367
366
|
|
|
@@ -384,7 +383,7 @@ const handleStop = async (input) => {
|
|
|
384
383
|
autoAppended: true,
|
|
385
384
|
continuityState: state.continuityState,
|
|
386
385
|
});
|
|
387
|
-
await
|
|
386
|
+
await maybeDeleteTrackedTurnState({ hookKey });
|
|
388
387
|
return null;
|
|
389
388
|
}
|
|
390
389
|
|
package/src/metrics.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import fsSync from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { getRepoMutationSafety } from './repo-safety.js';
|
|
5
5
|
import { countTokens } from './tokenCounter.js';
|
|
6
6
|
import { projectRoot } from './utils/paths.js';
|
|
7
7
|
import {
|
|
@@ -16,11 +16,6 @@ import {
|
|
|
16
16
|
const defaultMetricsDir = () => path.join(projectRoot, '.devctx');
|
|
17
17
|
const defaultMetricsFile = () => path.join(defaultMetricsDir(), 'metrics.jsonl');
|
|
18
18
|
const resolveEnvMetricsFile = () => process.env.DEVCTX_METRICS_FILE?.trim() || null;
|
|
19
|
-
const HARD_BLOCK_REPO_SAFETY_REASONS = [
|
|
20
|
-
['tracked', 'isTracked'],
|
|
21
|
-
['staged', 'isStaged'],
|
|
22
|
-
];
|
|
23
|
-
|
|
24
19
|
export const getMetricsFilePath = () => resolveEnvMetricsFile() ?? defaultMetricsFile();
|
|
25
20
|
|
|
26
21
|
let lastEnsuredDir = null;
|
|
@@ -68,6 +63,9 @@ export const getEntrySavingsPct = (
|
|
|
68
63
|
return rawTokens > 0 ? Number(((savedTokens / rawTokens) * 100).toFixed(2)) : 0;
|
|
69
64
|
};
|
|
70
65
|
|
|
66
|
+
export const getNetSavedTokens = (savedTokens, overheadTokens = 0) =>
|
|
67
|
+
Math.max(0, Number(savedTokens ?? 0) - Math.max(0, Number(overheadTokens ?? 0)));
|
|
68
|
+
|
|
71
69
|
export const MAX_METRICS_BYTES = 1024 * 1024;
|
|
72
70
|
export const KEEP_LINES_AFTER_ROTATION = 500;
|
|
73
71
|
|
|
@@ -89,16 +87,7 @@ const readActiveSessionIdFromDb = (db) =>
|
|
|
89
87
|
db.prepare('SELECT session_id FROM active_session WHERE scope = ?').get(ACTIVE_SESSION_SCOPE)?.session_id ?? null;
|
|
90
88
|
|
|
91
89
|
const getSqliteSafetyPolicy = () => {
|
|
92
|
-
|
|
93
|
-
const reasons = HARD_BLOCK_REPO_SAFETY_REASONS
|
|
94
|
-
.filter(([, field]) => repoSafety[field])
|
|
95
|
-
.map(([reason]) => reason);
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
repoSafety,
|
|
99
|
-
shouldBlock: reasons.length > 0,
|
|
100
|
-
reasons,
|
|
101
|
-
};
|
|
90
|
+
return getRepoMutationSafety();
|
|
102
91
|
};
|
|
103
92
|
|
|
104
93
|
export const getActiveSessionId = async () => {
|
|
@@ -258,20 +247,34 @@ export const aggregateMetrics = (entries) => {
|
|
|
258
247
|
|
|
259
248
|
const tools = [...byTool.values()]
|
|
260
249
|
.map((item) => ({
|
|
250
|
+
constOverhead: overheadByTool.get(item.tool)?.overheadTokens ?? 0,
|
|
261
251
|
...item,
|
|
252
|
+
}))
|
|
253
|
+
.map((item) => ({
|
|
254
|
+
...item,
|
|
255
|
+
overheadTokens: item.constOverhead,
|
|
256
|
+
netSavedTokens: getNetSavedTokens(item.savedTokens, item.constOverhead),
|
|
262
257
|
savingsPct: item.rawTokens > 0 ? Number(((item.savedTokens / item.rawTokens) * 100).toFixed(2)) : 0,
|
|
258
|
+
netSavingsPct: item.rawTokens > 0
|
|
259
|
+
? Number(((getNetSavedTokens(item.savedTokens, item.constOverhead) / item.rawTokens) * 100).toFixed(2))
|
|
260
|
+
: 0,
|
|
263
261
|
}))
|
|
262
|
+
.map(({ constOverhead, ...item }) => item)
|
|
264
263
|
.sort((a, b) => b.savedTokens - a.savedTokens || b.count - a.count || a.tool.localeCompare(b.tool));
|
|
265
264
|
|
|
266
265
|
const overheadTools = [...overheadByTool.values()]
|
|
267
266
|
.sort((a, b) => b.overheadTokens - a.overheadTokens || b.count - a.count || a.tool.localeCompare(b.tool));
|
|
268
267
|
|
|
268
|
+
const netSavedTokens = getNetSavedTokens(savedTokens, overheadTokens);
|
|
269
|
+
|
|
269
270
|
return {
|
|
270
271
|
count: entries.length,
|
|
271
272
|
rawTokens,
|
|
272
273
|
compressedTokens,
|
|
273
274
|
savedTokens,
|
|
274
275
|
savingsPct: rawTokens > 0 ? Number(((savedTokens / rawTokens) * 100).toFixed(2)) : 0,
|
|
276
|
+
netSavedTokens,
|
|
277
|
+
netSavingsPct: rawTokens > 0 ? Number(((netSavedTokens / rawTokens) * 100).toFixed(2)) : 0,
|
|
275
278
|
tools,
|
|
276
279
|
overheadTokens,
|
|
277
280
|
overheadPctOfRaw: rawTokens > 0 ? Number(((overheadTokens / rawTokens) * 100).toFixed(2)) : 0,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { persistMetrics } from '../metrics.js';
|
|
3
3
|
import { countTokens } from '../tokenCounter.js';
|
|
4
|
+
import { buildOperationalContextLines } from '../client-contract.js';
|
|
4
5
|
import { smartSummary } from '../tools/smart-summary.js';
|
|
5
6
|
import { smartTurn } from '../tools/smart-turn.js';
|
|
6
7
|
|
|
@@ -39,30 +40,13 @@ const extractNextStep = (value) => {
|
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
const buildContextLines = (startResult) => {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (summary.goal) {
|
|
50
|
-
lines.push(`Goal: ${truncate(summary.goal, 120)}`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (summary.currentFocus) {
|
|
54
|
-
lines.push(`Focus: ${truncate(summary.currentFocus, 120)}`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (summary.nextStep) {
|
|
58
|
-
lines.push(`Next step: ${truncate(summary.nextStep, 120)}`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (startResult?.continuity?.reason) {
|
|
62
|
-
lines.push(`Context status: ${truncate(startResult.continuity.reason, 120)}`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return lines.slice(0, 5);
|
|
43
|
+
const context = buildOperationalContextLines(startResult, {
|
|
44
|
+
sessionStart: false,
|
|
45
|
+
maxLineLength: 120,
|
|
46
|
+
maxLines: 8,
|
|
47
|
+
maxChars: 560,
|
|
48
|
+
});
|
|
49
|
+
return context ? context.split('\n') : [];
|
|
66
50
|
};
|
|
67
51
|
|
|
68
52
|
export const buildWrappedPrompt = ({ prompt, startResult }) => {
|
|
@@ -95,7 +79,16 @@ const ensureIsolatedSession = async ({ prompt, sessionId, startResult }) => {
|
|
|
95
79
|
if (sessionId || !startResult?.sessionId) {
|
|
96
80
|
return {
|
|
97
81
|
startResult,
|
|
98
|
-
isolated:
|
|
82
|
+
isolated: Boolean(startResult?.isolatedSession),
|
|
83
|
+
previousSessionId: startResult?.previousSessionId ?? null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (startResult?.isolatedSession) {
|
|
88
|
+
return {
|
|
89
|
+
startResult,
|
|
90
|
+
isolated: true,
|
|
91
|
+
previousSessionId: startResult.previousSessionId ?? null,
|
|
99
92
|
};
|
|
100
93
|
}
|
|
101
94
|
|
|
@@ -200,6 +193,7 @@ export const runHeadlessWrapper = async ({
|
|
|
200
193
|
dryRun = false,
|
|
201
194
|
streamOutput = false,
|
|
202
195
|
runCommand = runChildProcess,
|
|
196
|
+
preparedStartResult = null,
|
|
203
197
|
} = {}) => {
|
|
204
198
|
if (!normalizeWhitespace(prompt)) {
|
|
205
199
|
throw new Error('prompt is required');
|
|
@@ -209,7 +203,7 @@ export const runHeadlessWrapper = async ({
|
|
|
209
203
|
throw new Error('command is required unless dryRun=true');
|
|
210
204
|
}
|
|
211
205
|
|
|
212
|
-
const start = await smartTurn({
|
|
206
|
+
const start = preparedStartResult ?? await smartTurn({
|
|
213
207
|
phase: 'start',
|
|
214
208
|
sessionId,
|
|
215
209
|
prompt,
|
package/src/repo-safety.js
CHANGED
|
@@ -4,6 +4,11 @@ import { execFileSync } from 'node:child_process';
|
|
|
4
4
|
import { getStateDbPath } from './storage/sqlite.js';
|
|
5
5
|
import { projectRoot } from './utils/runtime-config.js';
|
|
6
6
|
|
|
7
|
+
export const HARD_BLOCK_REPO_SAFETY_REASONS = Object.freeze([
|
|
8
|
+
['tracked', 'isTracked'],
|
|
9
|
+
['staged', 'isStaged'],
|
|
10
|
+
]);
|
|
11
|
+
|
|
7
12
|
const hasGitignoreEntry = (content, entry) => {
|
|
8
13
|
const target = entry.replace(/\/+$/, '');
|
|
9
14
|
return content
|
|
@@ -164,3 +169,19 @@ export const enforceRepoSafety = ({
|
|
|
164
169
|
: 'Repository safety checks failed.',
|
|
165
170
|
};
|
|
166
171
|
};
|
|
172
|
+
|
|
173
|
+
export const getRepoMutationSafety = ({
|
|
174
|
+
root = projectRoot,
|
|
175
|
+
stateDbPath = getStateDbPath(),
|
|
176
|
+
} = {}) => {
|
|
177
|
+
const repoSafety = enforceRepoSafety({ root, stateDbPath });
|
|
178
|
+
const reasons = HARD_BLOCK_REPO_SAFETY_REASONS
|
|
179
|
+
.filter(([, field]) => repoSafety[field])
|
|
180
|
+
.map(([reason]) => reason);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
repoSafety,
|
|
184
|
+
shouldBlock: reasons.length > 0,
|
|
185
|
+
reasons,
|
|
186
|
+
};
|
|
187
|
+
};
|
package/src/server.js
CHANGED
|
@@ -10,6 +10,7 @@ import { smartReadBatch } from './tools/smart-read-batch.js';
|
|
|
10
10
|
import { smartShell } from './tools/smart-shell.js';
|
|
11
11
|
import { smartSummary } from './tools/smart-summary.js';
|
|
12
12
|
import { smartStatus } from './tools/smart-status.js';
|
|
13
|
+
import { smartDoctor } from './tools/smart-doctor.js';
|
|
13
14
|
import { smartEdit } from './tools/smart-edit.js';
|
|
14
15
|
import { smartMetrics } from './tools/smart-metrics.js';
|
|
15
16
|
import { smartTurn } from './tools/smart-turn.js';
|
|
@@ -379,7 +380,7 @@ This ensures optimal performance and context recovery.`,
|
|
|
379
380
|
|
|
380
381
|
server.tool(
|
|
381
382
|
'smart_summary',
|
|
382
|
-
'Maintain compressed conversation state across turns. Actions: get (retrieve current/last session), update (create or replace a session; omitted fields are cleared), append (add to existing session), auto_append (append only if something meaningful changed), checkpoint (event-driven orchestration that decides whether to auto-persist), reset (clear session), list_sessions (show all sessions), compact (apply retention/compaction to SQLite events), cleanup_legacy (inspect or remove imported legacy JSON/JSONL files). Sessions persist in project-local SQLite with 30-day retention defaults. Auto-generates sessionId from goal if omitted. `get` auto-resumes the active session when present, otherwise falls back to the best saved session when unambiguous; pass `sessionId: "auto"` to accept the recommended session even when multiple recent candidates exist. Returns a resume summary capped at maxTokens (default 500) plus compression metadata (`truncated`, `compressionLevel`, `omitted`) and `schemaVersion`.
|
|
383
|
+
'Maintain compressed conversation state across turns. Actions: get (retrieve current/last session), update (create or replace a session; omitted fields are cleared), append (add to existing session), auto_append (append only if something meaningful changed), checkpoint (event-driven orchestration that decides whether to auto-persist), reset (clear session), list_sessions (show all sessions), compact (apply retention/compaction to SQLite events), cleanup_legacy (inspect or remove imported legacy JSON/JSONL files). Sessions persist in project-local SQLite with 30-day retention defaults. Auto-generates sessionId from goal if omitted. `get` auto-resumes the active session when present, otherwise falls back to the best saved session when unambiguous; pass `sessionId: "auto"` to accept the recommended session even when multiple recent candidates exist. Returns a resume summary capped at maxTokens (default 500) plus compression metadata (`truncated`, `compressionLevel`, `omitted`) and `schemaVersion`. Exposes `repoSafety`, `mutationSafety`, `degradedMode`, and `storageHealth` so agents can detect blocked, read-only, missing, oversized, locked, or corrupted local state. Tracks: goal, status, pinned context, unresolved questions, current focus, blockers, next step, completed steps, key decisions, and touched files.',
|
|
383
384
|
{
|
|
384
385
|
action: z.enum(['get', 'update', 'append', 'auto_append', 'checkpoint', 'reset', 'list_sessions', 'compact', 'cleanup_legacy']),
|
|
385
386
|
sessionId: z.string().optional(),
|
|
@@ -445,7 +446,7 @@ This ensures optimal performance and context recovery.`,
|
|
|
445
446
|
|
|
446
447
|
server.tool(
|
|
447
448
|
'smart_status',
|
|
448
|
-
'Display the current session context including goal, status, recent decisions, touched files, and progress. Returns a formatted summary of what has been done and what is being tracked in the active session. Use this to understand the current state of work without modifying the session. Supports format=detailed (default, full formatted output) or format=compact (minimal JSON). Optional maxItems limits how many recent items to show (default 10).',
|
|
449
|
+
'Display the current session context including goal, status, recent decisions, touched files, and progress. Returns a formatted summary of what has been done and what is being tracked in the active session. Use this to understand the current state of work without modifying the session. Supports format=detailed (default, full formatted output) or format=compact (minimal JSON). Optional maxItems limits how many recent items to show (default 10). Exposes `mutationSafety`, `repoSafety`, `degradedMode`, and `storageHealth` when repo-safety or SQLite storage health affects session reads.',
|
|
449
450
|
{
|
|
450
451
|
format: z.enum(['detailed', 'compact']).optional(),
|
|
451
452
|
maxItems: z.number().int().min(1).max(50).optional(),
|
|
@@ -453,6 +454,15 @@ This ensures optimal performance and context recovery.`,
|
|
|
453
454
|
async ({ format, maxItems }) => asTextResult(await smartStatus({ format, maxItems })),
|
|
454
455
|
);
|
|
455
456
|
|
|
457
|
+
server.tool(
|
|
458
|
+
'smart_doctor',
|
|
459
|
+
'Run an operational health check for local devctx state. Aggregates repo hygiene, SQLite `storageHealth`, retention/compaction hygiene, and legacy JSON/JSONL cleanup guidance into one inspect-only result with explicit remediation steps. Use this when `.devctx/state.sqlite` is missing, oversized, locked, corrupted, or when you want a release/preflight check for local state durability.',
|
|
460
|
+
{
|
|
461
|
+
verifyIntegrity: z.boolean().optional(),
|
|
462
|
+
},
|
|
463
|
+
async ({ verifyIntegrity }) => asTextResult(await smartDoctor({ verifyIntegrity })),
|
|
464
|
+
);
|
|
465
|
+
|
|
456
466
|
server.tool(
|
|
457
467
|
'smart_edit',
|
|
458
468
|
'Batch edit multiple files with pattern replacement. Supports literal string replacement or regex patterns. Use for bulk refactoring, removing patterns (comments, console.log, etc.), or renaming across files. Optional dryRun shows preview without modifying files. Returns match count and results per file.',
|
|
@@ -469,7 +479,7 @@ This ensures optimal performance and context recovery.`,
|
|
|
469
479
|
|
|
470
480
|
server.tool(
|
|
471
481
|
'smart_turn',
|
|
472
|
-
'Orchestrate start/end of a meaningful agent turn so context usage becomes almost mandatory with low token overhead. `phase: "start"` rehydrates persisted context, classifies prompt continuity against the saved session, optionally auto-creates a planning session for a new substantial task, and can include compact metrics. `phase: "end"` writes a checkpoint through smart_summary and can optionally include compact metrics. Use this instead of manually chaining `smart_summary(get)` and `smart_summary(checkpoint)` when you want a single context-first turn workflow.',
|
|
482
|
+
'Orchestrate start/end of a meaningful agent turn so context usage becomes almost mandatory with low token overhead. `phase: "start"` rehydrates persisted context, classifies prompt continuity against the saved session, optionally auto-creates a planning session for a new substantial task, returns `recommendedPath` guidance for the next safe devctx actions, and can include compact metrics. `phase: "end"` writes a checkpoint through smart_summary, returns follow-up `recommendedPath` guidance, and can optionally include compact metrics. Both phases expose `mutationSafety` when repo-safety blocks persisted writes and now surface `storageHealth` when SQLite state is missing, oversized, locked, or corrupted. Use this instead of manually chaining `smart_summary(get)` and `smart_summary(checkpoint)` when you want a single context-first turn workflow.',
|
|
473
483
|
{
|
|
474
484
|
phase: z.enum(['start', 'end']),
|
|
475
485
|
sessionId: z.string().optional(),
|
|
@@ -513,7 +523,7 @@ This ensures optimal performance and context recovery.`,
|
|
|
513
523
|
|
|
514
524
|
server.tool(
|
|
515
525
|
'smart_metrics',
|
|
516
|
-
'Inspect token metrics recorded in project-local SQLite storage by default. Returns aggregated totals, per-tool savings, and
|
|
526
|
+
'Inspect token metrics recorded in project-local SQLite storage by default. Returns aggregated totals, per-tool savings, recent entries, adoption analysis, and `productQuality` signals measured from `smart_turn` orchestration events (continuity recovery, blocked-state remediation coverage, context-refresh signals, checkpoint persistence). Supports time windows (`24h`, `7d`, `30d`, `all`), optional tool filtering, and optional session filtering (`sessionId: "active"` resolves the current active session automatically). Pass `file` to inspect a legacy/custom JSONL file explicitly. When `.devctx/state.sqlite` is tracked or staged, reads fall back to a temporary read-only snapshot and expose `repoSafety`, `mutationSafety`, `degradedMode`, `sideEffectsSuppressed`, and `storageHealth`; metrics writes from other tools are skipped until git hygiene is fixed.',
|
|
517
527
|
{
|
|
518
528
|
file: z.string().optional(),
|
|
519
529
|
tool: z.string().optional(),
|