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.
@@ -7,6 +7,7 @@ import { projectRoot } from '../utils/runtime-config.js';
7
7
  export const STATE_DB_FILENAME = 'state.sqlite';
8
8
  export const SQLITE_SCHEMA_VERSION = 5;
9
9
  export const ACTIVE_SESSION_SCOPE = 'project';
10
+ export const STATE_DB_SOFT_MAX_BYTES = 32 * 1024 * 1024;
10
11
  export const EXPECTED_TABLES = [
11
12
  'active_session',
12
13
  'context_access',
@@ -191,6 +192,229 @@ const ensureStateDir = (filePath) => {
191
192
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
192
193
  };
193
194
 
195
+ const toRelativeStatePath = (filePath) =>
196
+ path.relative(projectRoot, filePath).replace(/\\/g, '/') || path.basename(filePath);
197
+
198
+ const buildStorageDiagnostic = ({
199
+ filePath,
200
+ issue,
201
+ status,
202
+ exists,
203
+ sizeBytes,
204
+ walExists,
205
+ shmExists,
206
+ message,
207
+ recommendedActions,
208
+ integrity = null,
209
+ details = {},
210
+ }) => ({
211
+ filePath,
212
+ relativePath: toRelativeStatePath(filePath),
213
+ issue,
214
+ status,
215
+ exists,
216
+ sizeBytes,
217
+ walExists,
218
+ shmExists,
219
+ softMaxBytes: STATE_DB_SOFT_MAX_BYTES,
220
+ message,
221
+ recommendedActions,
222
+ integrity,
223
+ ...details,
224
+ });
225
+
226
+ export const getStateStorageHealth = ({ filePath = getStateDbPath() } = {}) => {
227
+ const exists = fs.existsSync(filePath);
228
+ const sizeBytes = exists ? fs.statSync(filePath).size : 0;
229
+ const walExists = fs.existsSync(`${filePath}-wal`);
230
+ const shmExists = fs.existsSync(`${filePath}-shm`);
231
+
232
+ if (!exists) {
233
+ return buildStorageDiagnostic({
234
+ filePath,
235
+ issue: 'missing',
236
+ status: 'warning',
237
+ exists,
238
+ sizeBytes,
239
+ walExists,
240
+ shmExists,
241
+ message: 'Project-local SQLite state does not exist yet.',
242
+ recommendedActions: [
243
+ 'Run a persisted devctx action such as smart_summary update or smart_turn end to initialize state.sqlite.',
244
+ 'If you expected prior context, verify DEVCTX_STATE_DB_PATH / project root and that .devctx/ was not deleted.',
245
+ ],
246
+ });
247
+ }
248
+
249
+ if (sizeBytes > STATE_DB_SOFT_MAX_BYTES) {
250
+ return buildStorageDiagnostic({
251
+ filePath,
252
+ issue: 'oversized',
253
+ status: 'warning',
254
+ exists,
255
+ sizeBytes,
256
+ walExists,
257
+ shmExists,
258
+ message: 'Project-local SQLite state is larger than the recommended soft limit.',
259
+ recommendedActions: [
260
+ 'Run smart_summary compact to prune retained events and metrics.',
261
+ 'Archive or remove old local state after backing it up if the repository has become long-lived.',
262
+ ],
263
+ });
264
+ }
265
+
266
+ return buildStorageDiagnostic({
267
+ filePath,
268
+ issue: 'ok',
269
+ status: 'ok',
270
+ exists,
271
+ sizeBytes,
272
+ walExists,
273
+ shmExists,
274
+ message: 'Project-local SQLite state is present and within the recommended size range.',
275
+ recommendedActions: [],
276
+ });
277
+ };
278
+
279
+ export const classifyStateDbError = (error, { filePath = getStateDbPath(), readOnly = false } = {}) => {
280
+ const base = getStateStorageHealth({ filePath });
281
+ const message = String(error?.message ?? error ?? '');
282
+
283
+ if (/node:sqlite support|Node 22\+/i.test(message)) {
284
+ return buildStorageDiagnostic({
285
+ ...base,
286
+ filePath,
287
+ issue: 'unavailable',
288
+ status: 'error',
289
+ message: 'SQLite runtime support is unavailable in this Node.js process.',
290
+ recommendedActions: [
291
+ 'Use Node 22+ for SQLite-backed state.',
292
+ 'If you must stay on an older runtime, fall back to legacy JSON/JSONL storage paths only.',
293
+ ],
294
+ details: { errorMessage: message, readOnly },
295
+ });
296
+ }
297
+
298
+ if (/database is locked|database table is locked|SQLITE_BUSY|SQLITE_LOCKED/i.test(message)) {
299
+ return buildStorageDiagnostic({
300
+ ...base,
301
+ filePath,
302
+ issue: 'locked',
303
+ status: 'error',
304
+ message: 'Project-local SQLite state is locked by another process or unfinished transaction.',
305
+ recommendedActions: [
306
+ 'Close other devctx processes or wait for the active write to finish, then retry.',
307
+ 'Use snapshot-backed reads temporarily if you only need diagnostics while the lock clears.',
308
+ ],
309
+ details: { errorMessage: message, readOnly, retriable: true },
310
+ });
311
+ }
312
+
313
+ if (/file is not a database|database disk image is malformed|malformed/i.test(message)) {
314
+ return buildStorageDiagnostic({
315
+ ...base,
316
+ filePath,
317
+ issue: 'corrupted',
318
+ status: 'error',
319
+ message: 'Project-local SQLite state appears corrupted or unreadable as a database.',
320
+ recommendedActions: [
321
+ 'Back up .devctx/state.sqlite before removing or replacing it.',
322
+ 'Delete the corrupted state file and let devctx recreate it, then re-import any legacy state if available.',
323
+ ],
324
+ details: { errorMessage: message, readOnly, retriable: false },
325
+ });
326
+ }
327
+
328
+ if (!base.exists && (readOnly || /ENOENT|no such file|unable to open database file/i.test(message))) {
329
+ return buildStorageDiagnostic({
330
+ ...base,
331
+ filePath,
332
+ issue: 'missing',
333
+ status: 'warning',
334
+ message: 'Project-local SQLite state is missing for a read-only/open request.',
335
+ recommendedActions: [
336
+ 'Run a persisted devctx action to initialize state.sqlite.',
337
+ 'Verify the configured state path if the file should already exist.',
338
+ ],
339
+ details: { errorMessage: message, readOnly },
340
+ });
341
+ }
342
+
343
+ return buildStorageDiagnostic({
344
+ ...base,
345
+ filePath,
346
+ issue: 'unknown',
347
+ status: 'error',
348
+ message: 'Project-local SQLite state failed an unexpected storage operation.',
349
+ recommendedActions: [
350
+ 'Retry once to rule out a transient failure.',
351
+ 'If the problem persists, inspect or back up .devctx/state.sqlite before removing it.',
352
+ ],
353
+ details: { errorMessage: message, readOnly },
354
+ });
355
+ };
356
+
357
+ export const diagnoseStateStorage = async ({
358
+ filePath = getStateDbPath(),
359
+ verifyIntegrity = true,
360
+ } = {}) => {
361
+ const base = getStateStorageHealth({ filePath });
362
+ if (!verifyIntegrity || !base.exists) {
363
+ return base;
364
+ }
365
+
366
+ try {
367
+ const { DatabaseSync } = await loadSqliteModule();
368
+ const db = new DatabaseSync(filePath, { readOnly: true });
369
+
370
+ try {
371
+ const quickCheckRows = db.prepare('PRAGMA quick_check(1)').all();
372
+ const integrity = quickCheckRows?.[0]?.quick_check ?? quickCheckRows?.[0]?.quickCheck ?? 'unknown';
373
+ const tables = listStateTables(db);
374
+ const missingTables = EXPECTED_TABLES.filter((table) => !tables.includes(table));
375
+
376
+ if (integrity !== 'ok' || missingTables.length > 0) {
377
+ return buildStorageDiagnostic({
378
+ ...base,
379
+ filePath,
380
+ issue: 'corrupted',
381
+ status: 'error',
382
+ message: integrity !== 'ok'
383
+ ? 'Project-local SQLite state failed integrity checks.'
384
+ : 'Project-local SQLite state is missing expected schema tables.',
385
+ recommendedActions: [
386
+ 'Back up .devctx/state.sqlite before attempting recovery.',
387
+ 'Delete and recreate the local state if integrity issues persist after retrying.',
388
+ ],
389
+ integrity,
390
+ details: { missingTables },
391
+ });
392
+ }
393
+
394
+ return {
395
+ ...base,
396
+ integrity: 'ok',
397
+ tableCount: tables.length,
398
+ missingTables: [],
399
+ };
400
+ } finally {
401
+ db.close();
402
+ }
403
+ } catch (error) {
404
+ return classifyStateDbError(error, { filePath, readOnly: true });
405
+ }
406
+ };
407
+
408
+ const enrichStateDbError = (error, { filePath = getStateDbPath(), readOnly = false } = {}) => {
409
+ const storageHealth = classifyStateDbError(error, { filePath, readOnly });
410
+ const enriched = new Error(`${storageHealth.message} Original error: ${String(error?.message ?? error ?? 'unknown')}`);
411
+ enriched.cause = error;
412
+ enriched.code = error?.code ?? null;
413
+ enriched.storageHealth = storageHealth;
414
+ enriched.stateDbIssue = storageHealth.issue;
415
+ return enriched;
416
+ };
417
+
194
418
  const loadSqliteModule = async () => {
195
419
  if (!sqliteModulePromise) {
196
420
  sqliteModulePromise = import('node:sqlite')
@@ -280,17 +504,21 @@ export const listStateTables = (db) =>
280
504
  `).all().map((row) => row.name);
281
505
 
282
506
  export const openStateDb = async ({ filePath = getStateDbPath(), readOnly = false } = {}) => {
283
- const { DatabaseSync } = await loadSqliteModule();
284
- if (!readOnly) {
285
- ensureStateDir(filePath);
286
- }
507
+ try {
508
+ const { DatabaseSync } = await loadSqliteModule();
509
+ if (!readOnly) {
510
+ ensureStateDir(filePath);
511
+ }
287
512
 
288
- const db = new DatabaseSync(filePath, readOnly ? { readOnly: true } : {});
289
- if (!readOnly) {
290
- applyPragmas(db);
291
- runStateMigrations(db);
513
+ const db = new DatabaseSync(filePath, readOnly ? { readOnly: true } : {});
514
+ if (!readOnly) {
515
+ applyPragmas(db);
516
+ runStateMigrations(db);
517
+ }
518
+ return db;
519
+ } catch (error) {
520
+ throw enrichStateDbError(error, { filePath, readOnly });
292
521
  }
293
- return db;
294
522
  };
295
523
 
296
524
  export const initializeStateDb = async ({ filePath = getStateDbPath() } = {}) => {
@@ -728,9 +956,10 @@ const upsertHookTurnRow = (db, record) => {
728
956
  );
729
957
  };
730
958
 
731
- export const getHookTurnState = async ({ filePath = getStateDbPath(), hookKey } = {}) => withStateDb((db) =>
732
- normalizeHookTurnRow(readHookTurnRow(db, hookKey))
733
- , { filePath });
959
+ export const getHookTurnState = async ({ filePath = getStateDbPath(), hookKey, readOnly = false } = {}) => {
960
+ const reader = readOnly ? withStateDbSnapshot : withStateDb;
961
+ return reader((db) => normalizeHookTurnRow(readHookTurnRow(db, hookKey)), { filePath });
962
+ };
734
963
 
735
964
  export const setHookTurnState = async ({ filePath = getStateDbPath(), hookKey, state } = {}) => withStateDb((db) => {
736
965
  const record = buildHookTurnRecord(hookKey, state);
@@ -0,0 +1,354 @@
1
+ const normalizeWhitespace = (value) => String(value ?? '').replace(/\s+/g, ' ').trim();
2
+
3
+ const truncate = (value, maxLength = 140) => {
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
+ const buildContinuePrompt = (prompt) => {
17
+ const normalized = normalizeWhitespace(prompt);
18
+ if (!normalized) {
19
+ return 'Continue the active devctx task using the persisted context, follow the next recommended step, and checkpoint the next milestone.';
20
+ }
21
+ return [
22
+ 'Continue the active devctx task using the persisted context and align with the next recommended step.',
23
+ '',
24
+ `User request: ${normalized}`,
25
+ ].join('\n');
26
+ };
27
+
28
+ export const RUNNER_COMMANDS = Object.freeze([
29
+ 'task',
30
+ 'implement',
31
+ 'continue',
32
+ 'resume',
33
+ 'review',
34
+ 'debug',
35
+ 'refactor',
36
+ 'test',
37
+ 'doctor',
38
+ 'status',
39
+ 'checkpoint',
40
+ 'cleanup',
41
+ ]);
42
+
43
+ const DOCTOR_REQUIRED_STORAGE_ISSUES = new Set([
44
+ 'locked',
45
+ 'corrupted',
46
+ 'unavailable',
47
+ 'unknown',
48
+ ]);
49
+
50
+ export const WORKFLOW_COMMANDS = new Set([
51
+ 'task',
52
+ 'implement',
53
+ 'continue',
54
+ 'resume',
55
+ 'review',
56
+ 'debug',
57
+ 'refactor',
58
+ 'test',
59
+ ]);
60
+
61
+ export const SPECIALIZED_WORKFLOW_COMMANDS = new Set([
62
+ 'implement',
63
+ 'review',
64
+ 'debug',
65
+ 'refactor',
66
+ 'test',
67
+ ]);
68
+
69
+ export const WORKFLOW_DEFINITIONS = Object.freeze({
70
+ task: {
71
+ label: 'generic task',
72
+ defaultEvent: 'milestone',
73
+ requirePrompt: true,
74
+ workflowIntent: 'implementation',
75
+ policyMode: 'task_guided',
76
+ nextTools: ['smart_context', 'smart_read', 'smart_turn(end)'],
77
+ checkpointStrategy: 'Checkpoint the first meaningful milestone after you validate the working set.',
78
+ preflight: {
79
+ tool: 'smart_context',
80
+ detail: 'minimal',
81
+ include: ['hints', 'graph'],
82
+ maxTokens: 1200,
83
+ },
84
+ buildPrompt: (prompt) => normalizeWhitespace(prompt),
85
+ },
86
+ implement: {
87
+ label: 'implementation task',
88
+ defaultEvent: 'milestone',
89
+ requirePrompt: true,
90
+ workflowIntent: 'implementation',
91
+ policyMode: 'implement_guided',
92
+ nextTools: ['smart_context', 'smart_read(symbol)', 'smart_turn(end)'],
93
+ checkpointStrategy: 'Checkpoint after the first real implementation slice is integrated or behavior changes materially.',
94
+ preflight: {
95
+ tool: 'smart_context',
96
+ detail: 'minimal',
97
+ include: ['hints', 'graph', 'symbolDetail'],
98
+ maxTokens: 1400,
99
+ },
100
+ buildPrompt: (prompt) => {
101
+ const normalized = normalizeWhitespace(prompt);
102
+ return [
103
+ 'Implement this change using devctx context first.',
104
+ 'Prefer compact dependency-aware context and symbol reads before broad file edits.',
105
+ '',
106
+ `Implementation target: ${normalized}`,
107
+ ].join('\n');
108
+ },
109
+ },
110
+ continue: {
111
+ label: 'continue task',
112
+ defaultEvent: 'milestone',
113
+ requirePrompt: false,
114
+ workflowIntent: 'implementation',
115
+ policyMode: 'continue_guided',
116
+ nextTools: ['smart_turn(start)', 'smart_context', 'smart_turn(end)'],
117
+ checkpointStrategy: 'Continue until you reach the next concrete milestone, then checkpoint it.',
118
+ preflight: {
119
+ tool: 'smart_context',
120
+ detail: 'minimal',
121
+ include: ['hints', 'graph'],
122
+ maxTokens: 1200,
123
+ },
124
+ buildPrompt: buildContinuePrompt,
125
+ },
126
+ resume: {
127
+ label: 'resume task',
128
+ defaultEvent: 'milestone',
129
+ requirePrompt: false,
130
+ workflowIntent: 'implementation',
131
+ policyMode: 'resume_guided',
132
+ nextTools: ['smart_turn(start)', 'smart_context', 'smart_turn(end)'],
133
+ checkpointStrategy: 'Resume the active session and checkpoint the next meaningful slice rather than every minor step.',
134
+ preflight: {
135
+ tool: 'smart_context',
136
+ detail: 'minimal',
137
+ include: ['hints', 'graph'],
138
+ maxTokens: 1200,
139
+ },
140
+ buildPrompt: buildContinuePrompt,
141
+ },
142
+ review: {
143
+ label: 'code review',
144
+ defaultEvent: 'milestone',
145
+ requirePrompt: false,
146
+ workflowIntent: 'explore',
147
+ policyMode: 'review_guided',
148
+ nextTools: ['smart_context', 'smart_read', 'smart_turn(end)'],
149
+ checkpointStrategy: 'Checkpoint only after concrete findings, risk assessment, or a completed review slice.',
150
+ preflight: {
151
+ tool: 'smart_context',
152
+ detail: 'minimal',
153
+ include: ['hints', 'graph'],
154
+ maxTokens: 1200,
155
+ },
156
+ buildPrompt: (prompt) => {
157
+ const normalized = normalizeWhitespace(prompt) || 'Review the relevant diff or touched code path and surface concrete findings first.';
158
+ return [
159
+ 'Perform a code review using devctx context first.',
160
+ 'Prefer diff-aware context (`smart_context(diff=true)`) and compact reads before full-file reads.',
161
+ '',
162
+ `Review target: ${normalized}`,
163
+ ].join('\n');
164
+ },
165
+ },
166
+ debug: {
167
+ label: 'debugging task',
168
+ defaultEvent: 'milestone',
169
+ requirePrompt: false,
170
+ workflowIntent: 'debug',
171
+ policyMode: 'debug_guided',
172
+ nextTools: ['smart_search(intent=debug)', 'smart_read(symbol)', 'smart_turn(end)'],
173
+ checkpointStrategy: 'Checkpoint after the root cause is isolated or after the fix path is narrowed materially.',
174
+ preflight: {
175
+ tool: 'smart_search',
176
+ intent: 'debug',
177
+ },
178
+ buildPrompt: (prompt) => {
179
+ const normalized = normalizeWhitespace(prompt) || 'Investigate the failing path, identify the root cause, and propose or apply the smallest correct fix.';
180
+ return [
181
+ 'Debug this issue using devctx search and compact reads first.',
182
+ 'Prefer `smart_search(intent=debug)` and `smart_read(symbol)` before broad full-file reads.',
183
+ '',
184
+ `Debug target: ${normalized}`,
185
+ ].join('\n');
186
+ },
187
+ },
188
+ refactor: {
189
+ label: 'refactor task',
190
+ defaultEvent: 'milestone',
191
+ requirePrompt: false,
192
+ workflowIntent: 'implementation',
193
+ policyMode: 'refactor_guided',
194
+ nextTools: ['smart_context', 'smart_read(symbol)', 'smart_turn(end)'],
195
+ checkpointStrategy: 'Checkpoint each behavior-preserving slice once the dependency edges are revalidated.',
196
+ preflight: {
197
+ tool: 'smart_context',
198
+ detail: 'minimal',
199
+ include: ['hints', 'graph', 'symbolDetail'],
200
+ maxTokens: 1400,
201
+ },
202
+ buildPrompt: (prompt) => {
203
+ const normalized = normalizeWhitespace(prompt) || 'Refactor the target area while preserving behavior and validating the main dependency edges.';
204
+ return [
205
+ 'Refactor this area using devctx dependency-aware context first.',
206
+ 'Prefer compact context, symbol-level reads, and explicit checkpointing after each meaningful slice.',
207
+ '',
208
+ `Refactor target: ${normalized}`,
209
+ ].join('\n');
210
+ },
211
+ },
212
+ test: {
213
+ label: 'testing task',
214
+ defaultEvent: 'milestone',
215
+ requirePrompt: false,
216
+ workflowIntent: 'tests',
217
+ policyMode: 'test_guided',
218
+ nextTools: ['smart_search(intent=tests)', 'smart_read(symbol)', 'smart_turn(end)'],
219
+ checkpointStrategy: 'Checkpoint after the target tests are added, repaired, or conclusively validated.',
220
+ preflight: {
221
+ tool: 'smart_search',
222
+ intent: 'tests',
223
+ },
224
+ buildPrompt: (prompt) => {
225
+ const normalized = normalizeWhitespace(prompt) || 'Add or repair the relevant test coverage, then verify the main expected path.';
226
+ return [
227
+ 'Work in testing mode using devctx context first.',
228
+ 'Prefer `smart_search(intent=tests)` and targeted symbol reads before writing or updating tests.',
229
+ '',
230
+ `Testing target: ${normalized}`,
231
+ ].join('\n');
232
+ },
233
+ },
234
+ });
235
+
236
+ export const buildWorkflowPrompt = ({ commandName, prompt }) => {
237
+ const definition = WORKFLOW_DEFINITIONS[commandName];
238
+ if (!definition) {
239
+ throw new Error(`Unsupported workflow command: ${commandName}`);
240
+ }
241
+
242
+ const effectivePrompt = definition.buildPrompt(prompt ?? '');
243
+ if (definition.requirePrompt && !normalizeWhitespace(effectivePrompt)) {
244
+ throw new Error(`prompt is required for ${commandName}`);
245
+ }
246
+
247
+ return effectivePrompt;
248
+ };
249
+
250
+ export const buildWorkflowPolicyProfile = ({ commandName }) => {
251
+ const definition = WORKFLOW_DEFINITIONS[commandName];
252
+ if (!definition) {
253
+ throw new Error(`Unsupported workflow command: ${commandName}`);
254
+ }
255
+
256
+ return {
257
+ commandName,
258
+ label: definition.label,
259
+ workflowIntent: definition.workflowIntent ?? 'implementation',
260
+ policyMode: definition.policyMode ?? 'guided',
261
+ nextTools: [...(definition.nextTools ?? [])],
262
+ checkpointStrategy: definition.checkpointStrategy ?? null,
263
+ preflight: definition.preflight ? { ...definition.preflight } : null,
264
+ specialized: SPECIALIZED_WORKFLOW_COMMANDS.has(commandName),
265
+ defaultEvent: definition.defaultEvent ?? 'milestone',
266
+ };
267
+ };
268
+
269
+ export const evaluateRunnerGate = ({ startResult }) => {
270
+ const blocked = Boolean(startResult?.mutationSafety?.blocked);
271
+ const storageIssue = startResult?.storageHealth?.issue ?? 'ok';
272
+ const storageBlocked = DOCTOR_REQUIRED_STORAGE_ISSUES.has(storageIssue);
273
+ const pathBlocked = startResult?.recommendedPath?.mode === 'blocked_guided';
274
+
275
+ const reasons = [];
276
+ if (blocked) {
277
+ reasons.push('mutation_blocked');
278
+ }
279
+ if (storageBlocked) {
280
+ reasons.push(`storage_${storageIssue}`);
281
+ }
282
+ if (pathBlocked) {
283
+ reasons.push('recommended_path_blocked');
284
+ }
285
+
286
+ const requiresDoctor = blocked || storageBlocked || pathBlocked;
287
+
288
+ return {
289
+ blocked,
290
+ storageIssue,
291
+ pathBlocked,
292
+ requiresDoctor,
293
+ reasons,
294
+ };
295
+ };
296
+
297
+ export const buildBlockedRunnerMessage = ({ commandName, gate, doctorResult }) => {
298
+ const doctorMessage = normalizeWhitespace(doctorResult?.message ?? '');
299
+ if (doctorMessage) {
300
+ return doctorMessage;
301
+ }
302
+
303
+ if (gate.storageIssue !== 'ok') {
304
+ return `The ${commandName} workflow is paused until storageHealth.issue=${gate.storageIssue} is remediated.`;
305
+ }
306
+
307
+ if (gate.blocked) {
308
+ return `The ${commandName} workflow is paused until mutationSafety.blocked is remediated.`;
309
+ }
310
+
311
+ return `The ${commandName} workflow is paused until the operational state is remediated.`;
312
+ };
313
+
314
+ export const buildRunnerBlockedResult = ({
315
+ commandName,
316
+ client,
317
+ prompt,
318
+ startResult,
319
+ gate,
320
+ doctorResult,
321
+ allowDegraded,
322
+ workflowPolicy,
323
+ }) => ({
324
+ success: false,
325
+ command: commandName,
326
+ client,
327
+ prompt: truncate(prompt, 240),
328
+ blocked: true,
329
+ usedWrapper: false,
330
+ allowDegraded,
331
+ message: buildBlockedRunnerMessage({ commandName, gate, doctorResult }),
332
+ gate,
333
+ start: startResult,
334
+ doctor: doctorResult,
335
+ sessionId: startResult?.sessionId ?? null,
336
+ recommendedActions: doctorResult?.recommendedActions ?? startResult?.mutationSafety?.recommendedActions ?? [],
337
+ workflowPolicy,
338
+ });
339
+
340
+ export const buildCleanupPlan = ({
341
+ mode = 'compact',
342
+ apply = false,
343
+ retentionDays = 30,
344
+ keepLatestEventsPerSession = 20,
345
+ keepLatestMetrics = 1000,
346
+ vacuum = false,
347
+ }) => ({
348
+ mode,
349
+ apply,
350
+ retentionDays,
351
+ keepLatestEventsPerSession,
352
+ keepLatestMetrics,
353
+ vacuum,
354
+ });