sequant 1.15.2 → 1.15.3

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.
Files changed (43) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/bin/cli.js +3 -0
  3. package/dist/src/commands/logs.js +15 -0
  4. package/dist/src/commands/run.d.ts +114 -1
  5. package/dist/src/commands/run.js +447 -23
  6. package/dist/src/commands/stats.js +48 -0
  7. package/dist/src/lib/scope/index.d.ts +1 -0
  8. package/dist/src/lib/scope/index.js +2 -0
  9. package/dist/src/lib/scope/settings-converter.d.ts +28 -0
  10. package/dist/src/lib/scope/settings-converter.js +53 -0
  11. package/dist/src/lib/settings.d.ts +45 -0
  12. package/dist/src/lib/settings.js +30 -0
  13. package/dist/src/lib/test-tautology-detector.d.ts +122 -0
  14. package/dist/src/lib/test-tautology-detector.js +488 -0
  15. package/dist/src/lib/workflow/git-diff-utils.d.ts +39 -0
  16. package/dist/src/lib/workflow/git-diff-utils.js +142 -0
  17. package/dist/src/lib/workflow/log-writer.d.ts +9 -2
  18. package/dist/src/lib/workflow/log-writer.js +9 -3
  19. package/dist/src/lib/workflow/metrics-schema.d.ts +9 -0
  20. package/dist/src/lib/workflow/metrics-schema.js +10 -1
  21. package/dist/src/lib/workflow/phase-detection.d.ts +3 -0
  22. package/dist/src/lib/workflow/phase-detection.js +27 -1
  23. package/dist/src/lib/workflow/qa-cache.d.ts +3 -1
  24. package/dist/src/lib/workflow/qa-cache.js +2 -0
  25. package/dist/src/lib/workflow/run-log-schema.d.ts +86 -3
  26. package/dist/src/lib/workflow/run-log-schema.js +40 -2
  27. package/dist/src/lib/workflow/state-utils.d.ts +46 -0
  28. package/dist/src/lib/workflow/state-utils.js +167 -0
  29. package/dist/src/lib/workflow/token-utils.d.ts +92 -0
  30. package/dist/src/lib/workflow/token-utils.js +170 -0
  31. package/dist/src/lib/workflow/types.d.ts +6 -0
  32. package/dist/src/lib/workflow/types.js +1 -0
  33. package/package.json +1 -1
  34. package/templates/hooks/pre-tool.sh +4 -0
  35. package/templates/skills/assess/SKILL.md +1 -1
  36. package/templates/skills/exec/SKILL.md +5 -4
  37. package/templates/skills/improve/SKILL.md +37 -24
  38. package/templates/skills/loop/SKILL.md +3 -3
  39. package/templates/skills/qa/references/code-review-checklist.md +10 -11
  40. package/templates/skills/qa/scripts/quality-checks.sh +16 -0
  41. package/templates/skills/security-review/references/security-checklists.md +89 -36
  42. package/templates/skills/solve/SKILL.md +3 -1
  43. package/templates/skills/spec/SKILL.md +8 -4
@@ -248,6 +248,13 @@ function calculateMetricsAnalytics(metrics) {
248
248
  chainModeSuccessRate: 0,
249
249
  singleIssueSuccessRate: 0,
250
250
  insights: [],
251
+ // Token breakdown
252
+ totalInputTokens: 0,
253
+ totalOutputTokens: 0,
254
+ totalCacheTokens: 0,
255
+ avgInputTokens: 0,
256
+ avgOutputTokens: 0,
257
+ avgCacheTokens: 0,
251
258
  };
252
259
  }
253
260
  const successCount = runs.filter((r) => r.outcome === "success").length;
@@ -273,6 +280,10 @@ function calculateMetricsAnalytics(metrics) {
273
280
  : 0;
274
281
  // Generate insights
275
282
  const insights = generateInsights(runs, successRate, avgFilesChanged, avgLinesAdded, chainModeSuccessRate, singleIssueSuccessRate);
283
+ // Token breakdown (AC-10)
284
+ const totalInputTokens = runs.reduce((sum, r) => sum + (r.metrics.inputTokens ?? 0), 0);
285
+ const totalOutputTokens = runs.reduce((sum, r) => sum + (r.metrics.outputTokens ?? 0), 0);
286
+ const totalCacheTokens = runs.reduce((sum, r) => sum + (r.metrics.cacheTokens ?? 0), 0);
276
287
  return {
277
288
  totalRuns: runs.length,
278
289
  successCount,
@@ -286,6 +297,13 @@ function calculateMetricsAnalytics(metrics) {
286
297
  chainModeSuccessRate,
287
298
  singleIssueSuccessRate,
288
299
  insights,
300
+ // Token breakdown
301
+ totalInputTokens,
302
+ totalOutputTokens,
303
+ totalCacheTokens,
304
+ avgInputTokens: runs.length > 0 ? totalInputTokens / runs.length : 0,
305
+ avgOutputTokens: runs.length > 0 ? totalOutputTokens / runs.length : 0,
306
+ avgCacheTokens: runs.length > 0 ? totalCacheTokens / runs.length : 0,
289
307
  };
290
308
  }
291
309
  /**
@@ -375,6 +393,27 @@ function displayMetricsAnalytics(analytics) {
375
393
  }
376
394
  avgData["Duration"] = formatDuration(analytics.avgDuration);
377
395
  console.log(ui.keyValueTable(avgData));
396
+ // Token breakdown (AC-10)
397
+ const hasTokenBreakdown = analytics.totalInputTokens > 0 ||
398
+ analytics.totalOutputTokens > 0 ||
399
+ analytics.totalCacheTokens > 0;
400
+ if (hasTokenBreakdown) {
401
+ console.log(ui.sectionHeader("Token Usage"));
402
+ const tokenData = {};
403
+ if (analytics.totalInputTokens > 0) {
404
+ tokenData["Input tokens"] = analytics.totalInputTokens.toLocaleString();
405
+ tokenData["Avg input/run"] = Math.round(analytics.avgInputTokens).toLocaleString();
406
+ }
407
+ if (analytics.totalOutputTokens > 0) {
408
+ tokenData["Output tokens"] = analytics.totalOutputTokens.toLocaleString();
409
+ tokenData["Avg output/run"] = Math.round(analytics.avgOutputTokens).toLocaleString();
410
+ }
411
+ if (analytics.totalCacheTokens > 0) {
412
+ tokenData["Cache tokens"] = analytics.totalCacheTokens.toLocaleString();
413
+ tokenData["Avg cache/run"] = Math.round(analytics.avgCacheTokens).toLocaleString();
414
+ }
415
+ console.log(ui.keyValueTable(tokenData));
416
+ }
378
417
  // Insights
379
418
  if (analytics.insights.length > 0) {
380
419
  console.log(ui.sectionHeader("Insights"));
@@ -409,6 +448,15 @@ export async function statsCommand(options) {
409
448
  chainModeSuccessRate: analytics.chainModeSuccessRate,
410
449
  singleIssueSuccessRate: analytics.singleIssueSuccessRate,
411
450
  insights: analytics.insights,
451
+ // Token breakdown (AC-10)
452
+ tokenBreakdown: {
453
+ totalInputTokens: analytics.totalInputTokens,
454
+ totalOutputTokens: analytics.totalOutputTokens,
455
+ totalCacheTokens: analytics.totalCacheTokens,
456
+ avgInputTokens: analytics.avgInputTokens,
457
+ avgOutputTokens: analytics.avgOutputTokens,
458
+ avgCacheTokens: analytics.avgCacheTokens,
459
+ },
412
460
  runs: metrics.runs,
413
461
  };
414
462
  console.log(JSON.stringify(output, null, 2));
@@ -22,6 +22,7 @@ export { DEFAULT_SCOPE_CONFIG, ScopeAssessmentSchema } from "./types.js";
22
22
  export { clusterACByKeyword, detectTitleVerbs, estimateDirectorySpread, calculateFeatureCount, detectFeatures, parseNonGoals, shouldSkipAssessment, } from "./analyzer.js";
23
23
  export { getMetricStatus, createScopeMetrics, calculateVerdict, generateRecommendation, getVerdictEmoji, getStatusEmoji, shouldEnableQualityLoop, } from "./verdict.js";
24
24
  export { formatNonGoals, formatMetricsTable, formatVerdict, formatScopeAssessment, formatCondensedAssessment, } from "./formatter.js";
25
+ export { convertSettingsToConfig } from "./settings-converter.js";
25
26
  /**
26
27
  * Perform complete scope assessment
27
28
  *
@@ -25,6 +25,8 @@ export { clusterACByKeyword, detectTitleVerbs, estimateDirectorySpread, calculat
25
25
  export { getMetricStatus, createScopeMetrics, calculateVerdict, generateRecommendation, getVerdictEmoji, getStatusEmoji, shouldEnableQualityLoop, } from "./verdict.js";
26
26
  // Re-export formatter functions
27
27
  export { formatNonGoals, formatMetricsTable, formatVerdict, formatScopeAssessment, formatCondensedAssessment, } from "./formatter.js";
28
+ // Re-export settings converter
29
+ export { convertSettingsToConfig } from "./settings-converter.js";
28
30
  /**
29
31
  * Perform complete scope assessment
30
32
  *
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Settings to Config Converter
3
+ *
4
+ * Converts ScopeAssessmentSettings (from .sequant/settings.json)
5
+ * to ScopeAssessmentConfig (used by performScopeAssessment).
6
+ *
7
+ * This enables users to configure custom scope thresholds
8
+ * via their project settings file.
9
+ */
10
+ import type { ScopeAssessmentSettings } from "../settings.js";
11
+ import type { ScopeAssessmentConfig } from "./types.js";
12
+ /**
13
+ * Convert ScopeAssessmentSettings to ScopeAssessmentConfig
14
+ *
15
+ * Merges user settings with defaults to produce a valid config.
16
+ * Any missing fields are filled from DEFAULT_SCOPE_CONFIG.
17
+ *
18
+ * @param settings - User settings from .sequant/settings.json
19
+ * @returns Complete config ready for performScopeAssessment
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const settings = await getSettings();
24
+ * const config = convertSettingsToConfig(settings.scopeAssessment);
25
+ * const assessment = performScopeAssessment(criteria, body, title, config);
26
+ * ```
27
+ */
28
+ export declare function convertSettingsToConfig(settings?: Partial<ScopeAssessmentSettings>): ScopeAssessmentConfig;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Settings to Config Converter
3
+ *
4
+ * Converts ScopeAssessmentSettings (from .sequant/settings.json)
5
+ * to ScopeAssessmentConfig (used by performScopeAssessment).
6
+ *
7
+ * This enables users to configure custom scope thresholds
8
+ * via their project settings file.
9
+ */
10
+ import { DEFAULT_SCOPE_CONFIG } from "./types.js";
11
+ /**
12
+ * Convert ScopeAssessmentSettings to ScopeAssessmentConfig
13
+ *
14
+ * Merges user settings with defaults to produce a valid config.
15
+ * Any missing fields are filled from DEFAULT_SCOPE_CONFIG.
16
+ *
17
+ * @param settings - User settings from .sequant/settings.json
18
+ * @returns Complete config ready for performScopeAssessment
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const settings = await getSettings();
23
+ * const config = convertSettingsToConfig(settings.scopeAssessment);
24
+ * const assessment = performScopeAssessment(criteria, body, title, config);
25
+ * ```
26
+ */
27
+ export function convertSettingsToConfig(settings) {
28
+ // Handle undefined/null settings
29
+ if (!settings) {
30
+ return DEFAULT_SCOPE_CONFIG;
31
+ }
32
+ // Helper to merge threshold with defaults
33
+ const mergeThreshold = (userThreshold, defaultThreshold) => ({
34
+ yellow: userThreshold?.yellow ?? defaultThreshold?.yellow ?? 0,
35
+ red: userThreshold?.red ?? defaultThreshold?.red ?? 0,
36
+ });
37
+ return {
38
+ enabled: settings.enabled ?? DEFAULT_SCOPE_CONFIG.enabled,
39
+ skipIfSimple: settings.skipIfSimple ?? DEFAULT_SCOPE_CONFIG.skipIfSimple,
40
+ trivialThresholds: {
41
+ maxACItems: settings.trivialThresholds?.maxACItems ??
42
+ DEFAULT_SCOPE_CONFIG.trivialThresholds.maxACItems,
43
+ maxDirectories: settings.trivialThresholds?.maxDirectories ??
44
+ DEFAULT_SCOPE_CONFIG.trivialThresholds.maxDirectories,
45
+ },
46
+ thresholds: {
47
+ featureCount: mergeThreshold(settings.thresholds?.featureCount, DEFAULT_SCOPE_CONFIG.thresholds.featureCount),
48
+ acItems: mergeThreshold(settings.thresholds?.acItems, DEFAULT_SCOPE_CONFIG.thresholds.acItems),
49
+ fileEstimate: mergeThreshold(settings.thresholds?.fileEstimate, DEFAULT_SCOPE_CONFIG.thresholds.fileEstimate),
50
+ directorySpread: mergeThreshold(settings.thresholds?.directorySpread, DEFAULT_SCOPE_CONFIG.thresholds.directorySpread),
51
+ },
52
+ };
53
+ }
@@ -80,6 +80,13 @@ export interface RunSettings {
80
80
  * Default: true
81
81
  */
82
82
  mcp: boolean;
83
+ /**
84
+ * Enable automatic retry with MCP fallback.
85
+ * When true (default), failed phases are retried with MCP disabled.
86
+ * When false or --no-retry flag is used, no retry attempts are made.
87
+ * Default: true
88
+ */
89
+ retry: boolean;
83
90
  }
84
91
  /**
85
92
  * Scope assessment threshold configuration
@@ -90,14 +97,40 @@ export interface ScopeThreshold {
90
97
  /** Value at which status becomes red */
91
98
  red: number;
92
99
  }
100
+ /**
101
+ * Trivial issue thresholds for skipping scope assessment
102
+ */
103
+ export interface TrivialThresholds {
104
+ /**
105
+ * Maximum AC items for trivial classification.
106
+ * Issues with fewer AC items are considered trivial.
107
+ * Default: 3
108
+ */
109
+ maxACItems: number;
110
+ /**
111
+ * Maximum directories touched for trivial classification.
112
+ * Issues affecting fewer directories are considered trivial.
113
+ * Default: 1
114
+ */
115
+ maxDirectories: number;
116
+ }
93
117
  /**
94
118
  * Scope assessment settings
119
+ *
120
+ * Configuration for scope assessment during /spec phase.
121
+ * These settings control how issue scope is evaluated and
122
+ * what thresholds trigger warnings.
95
123
  */
96
124
  export interface ScopeAssessmentSettings {
97
125
  /** Whether scope assessment is enabled (default: true) */
98
126
  enabled: boolean;
99
127
  /** Skip assessment for trivial issues (default: true) */
100
128
  skipIfSimple: boolean;
129
+ /**
130
+ * Trivial issue thresholds (skip if below all).
131
+ * Issues that fall below all these thresholds are skipped.
132
+ */
133
+ trivialThresholds: TrivialThresholds;
101
134
  /** Thresholds for scope metrics */
102
135
  thresholds: {
103
136
  /** Feature count thresholds (default: yellow=2, red=3) */
@@ -106,6 +139,8 @@ export interface ScopeAssessmentSettings {
106
139
  acItems: ScopeThreshold;
107
140
  /** File estimate thresholds (default: yellow=8, red=13) */
108
141
  fileEstimate: ScopeThreshold;
142
+ /** Directory spread thresholds (default: yellow=3, red=5) */
143
+ directorySpread: ScopeThreshold;
109
144
  };
110
145
  }
111
146
  /**
@@ -129,8 +164,18 @@ export declare const DEFAULT_ROTATION_SETTINGS: RotationSettings;
129
164
  * Default agent settings (cost-optimized)
130
165
  */
131
166
  export declare const DEFAULT_AGENT_SETTINGS: AgentSettings;
167
+ /**
168
+ * Default trivial thresholds for scope assessment
169
+ *
170
+ * Issues that fall below ALL of these thresholds are considered trivial
171
+ * and scope assessment is skipped.
172
+ */
173
+ export declare const DEFAULT_TRIVIAL_THRESHOLDS: TrivialThresholds;
132
174
  /**
133
175
  * Default scope assessment settings
176
+ *
177
+ * These defaults match the values in DEFAULT_SCOPE_CONFIG from
178
+ * src/lib/scope/types.ts to ensure consistency.
134
179
  */
135
180
  export declare const DEFAULT_SCOPE_ASSESSMENT_SETTINGS: ScopeAssessmentSettings;
136
181
  /**
@@ -31,16 +31,41 @@ export const DEFAULT_AGENT_SETTINGS = {
31
31
  parallel: false,
32
32
  model: "haiku",
33
33
  };
34
+ /**
35
+ * Default trivial thresholds for scope assessment
36
+ *
37
+ * Issues that fall below ALL of these thresholds are considered trivial
38
+ * and scope assessment is skipped.
39
+ */
40
+ export const DEFAULT_TRIVIAL_THRESHOLDS = {
41
+ /** Issues with 3 or fewer AC items are potentially trivial */
42
+ maxACItems: 3,
43
+ /** Issues touching only 1 directory are potentially trivial */
44
+ maxDirectories: 1,
45
+ };
34
46
  /**
35
47
  * Default scope assessment settings
48
+ *
49
+ * These defaults match the values in DEFAULT_SCOPE_CONFIG from
50
+ * src/lib/scope/types.ts to ensure consistency.
36
51
  */
37
52
  export const DEFAULT_SCOPE_ASSESSMENT_SETTINGS = {
53
+ /** Enable scope assessment by default */
38
54
  enabled: true,
55
+ /** Skip assessment for trivial issues by default */
39
56
  skipIfSimple: true,
57
+ /** Trivial issue thresholds - skip if below all */
58
+ trivialThresholds: DEFAULT_TRIVIAL_THRESHOLDS,
59
+ /** Thresholds for scope metrics */
40
60
  thresholds: {
61
+ /** 2 features = yellow warning, 3+ = red (split recommended) */
41
62
  featureCount: { yellow: 2, red: 3 },
63
+ /** 6-8 AC items = yellow, 9+ = red */
42
64
  acItems: { yellow: 6, red: 9 },
65
+ /** 8-12 files estimated = yellow, 13+ = red */
43
66
  fileEstimate: { yellow: 8, red: 13 },
67
+ /** 3-4 directories = yellow, 5+ = red */
68
+ directorySpread: { yellow: 3, red: 5 },
44
69
  },
45
70
  };
46
71
  /**
@@ -59,6 +84,7 @@ export const DEFAULT_SETTINGS = {
59
84
  smartTests: true,
60
85
  rotation: DEFAULT_ROTATION_SETTINGS,
61
86
  mcp: true, // Enable MCP servers by default in headless mode
87
+ retry: true, // Enable automatic retry with MCP fallback by default
62
88
  },
63
89
  agents: DEFAULT_AGENT_SETTINGS,
64
90
  scopeAssessment: DEFAULT_SCOPE_ASSESSMENT_SETTINGS,
@@ -89,6 +115,10 @@ export async function getSettings() {
89
115
  scopeAssessment: {
90
116
  ...DEFAULT_SCOPE_ASSESSMENT_SETTINGS,
91
117
  ...parsed.scopeAssessment,
118
+ trivialThresholds: {
119
+ ...DEFAULT_SCOPE_ASSESSMENT_SETTINGS.trivialThresholds,
120
+ ...parsed.scopeAssessment?.trivialThresholds,
121
+ },
92
122
  thresholds: {
93
123
  ...DEFAULT_SCOPE_ASSESSMENT_SETTINGS.thresholds,
94
124
  ...parsed.scopeAssessment?.thresholds,
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Test Tautology Detector
3
+ *
4
+ * Detects tautological tests — tests that pass but don't call any production code.
5
+ * These tests provide zero regression protection as they only assert on local values.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { detectTautologicalTests, formatTautologyResults } from './test-tautology-detector';
10
+ *
11
+ * const results = detectTautologicalTests([
12
+ * { path: 'src/lib/foo.test.ts', content: fileContent },
13
+ * ]);
14
+ * console.log(formatTautologyResults(results));
15
+ * ```
16
+ */
17
+ /**
18
+ * Represents an imported function from a source module
19
+ */
20
+ export interface ImportedFunction {
21
+ /** Function name */
22
+ name: string;
23
+ /** Module path the function was imported from */
24
+ modulePath: string;
25
+ }
26
+ /**
27
+ * Represents a test block (it() or test())
28
+ */
29
+ export interface TestBlock {
30
+ /** Test description */
31
+ description: string;
32
+ /** Line number where the test starts */
33
+ lineNumber: number;
34
+ /** Whether this test is tautological (no production function calls) */
35
+ isTautological: boolean;
36
+ /** Style of test block: 'it' or 'test' */
37
+ style: "it" | "test";
38
+ }
39
+ /**
40
+ * Result of analyzing a single test file
41
+ */
42
+ export interface TautologyFileResult {
43
+ /** Path to the test file */
44
+ filePath: string;
45
+ /** Total number of test blocks found */
46
+ totalTests: number;
47
+ /** Number of tautological test blocks */
48
+ tautologicalCount: number;
49
+ /** Percentage of tests that are tautological */
50
+ tautologicalPercentage: number;
51
+ /** Individual test blocks with their analysis */
52
+ testBlocks: TestBlock[];
53
+ /** Imported functions from source modules */
54
+ importedFunctions: ImportedFunction[];
55
+ /** Whether the file could be parsed successfully */
56
+ parseSuccess: boolean;
57
+ /** Error message if parsing failed */
58
+ parseError?: string;
59
+ }
60
+ /**
61
+ * Overall tautology detection results
62
+ */
63
+ export interface TautologyResults {
64
+ /** Results for each analyzed file */
65
+ fileResults: TautologyFileResult[];
66
+ /** Summary statistics */
67
+ summary: {
68
+ totalFiles: number;
69
+ totalTests: number;
70
+ totalTautological: number;
71
+ overallPercentage: number;
72
+ /** Whether >50% of tests are tautological (blocking threshold) */
73
+ exceedsBlockingThreshold: boolean;
74
+ };
75
+ }
76
+ /**
77
+ * Check if an import path is from a source module (not a test library or mock)
78
+ */
79
+ export declare function isSourceModule(modulePath: string): boolean;
80
+ /**
81
+ * Extract imports from a test file
82
+ *
83
+ * Handles:
84
+ * - Named imports: `import { foo, bar } from './module'`
85
+ * - Default imports: `import foo from './module'`
86
+ * - Namespace imports: `import * as foo from './module'` (extracts the namespace name)
87
+ */
88
+ export declare function extractImports(content: string): ImportedFunction[];
89
+ /**
90
+ * Extract test blocks (it() and test()) from content
91
+ *
92
+ * Returns the description, line number, body content, and style of each test block.
93
+ */
94
+ export declare function extractTestBlocks(content: string): Array<{
95
+ description: string;
96
+ lineNumber: number;
97
+ body: string;
98
+ style: "it" | "test";
99
+ }>;
100
+ /**
101
+ * Check if a test block contains calls to any of the imported production functions
102
+ */
103
+ export declare function testBlockCallsProductionCode(body: string, importedFunctions: ImportedFunction[]): boolean;
104
+ /**
105
+ * Analyze a single test file for tautological tests
106
+ */
107
+ export declare function analyzeTestFile(content: string, filePath: string): TautologyFileResult;
108
+ /**
109
+ * Detect tautological tests across multiple files
110
+ */
111
+ export declare function detectTautologicalTests(files: Array<{
112
+ path: string;
113
+ content: string;
114
+ }>): TautologyResults;
115
+ /**
116
+ * Format tautology results as markdown for QA output
117
+ */
118
+ export declare function formatTautologyResults(results: TautologyResults): string;
119
+ /**
120
+ * Determine verdict impact based on tautology results
121
+ */
122
+ export declare function getTautologyVerdictImpact(results: TautologyResults): "none" | "warning" | "blocking";