@blockspool/cli 0.4.1 → 0.4.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 (149) hide show
  1. package/dist/bin/blockspool.d.ts +16 -0
  2. package/dist/bin/blockspool.d.ts.map +1 -0
  3. package/dist/bin/blockspool.js +45 -0
  4. package/dist/bin/blockspool.js.map +1 -0
  5. package/dist/commands/solo-auto.d.ts +6 -0
  6. package/dist/commands/solo-auto.d.ts.map +1 -0
  7. package/dist/commands/solo-auto.js +418 -0
  8. package/dist/commands/solo-auto.js.map +1 -0
  9. package/dist/commands/solo-exec.d.ts +6 -0
  10. package/dist/commands/solo-exec.d.ts.map +1 -0
  11. package/dist/commands/solo-exec.js +656 -0
  12. package/dist/commands/solo-exec.js.map +1 -0
  13. package/dist/commands/solo-inspect.d.ts +6 -0
  14. package/dist/commands/solo-inspect.d.ts.map +1 -0
  15. package/dist/commands/solo-inspect.js +690 -0
  16. package/dist/commands/solo-inspect.js.map +1 -0
  17. package/dist/commands/solo-lifecycle.d.ts +6 -0
  18. package/dist/commands/solo-lifecycle.d.ts.map +1 -0
  19. package/dist/commands/solo-lifecycle.js +188 -0
  20. package/dist/commands/solo-lifecycle.js.map +1 -0
  21. package/dist/commands/solo-nudge.d.ts +6 -0
  22. package/dist/commands/solo-nudge.d.ts.map +1 -0
  23. package/dist/commands/solo-nudge.js +49 -0
  24. package/dist/commands/solo-nudge.js.map +1 -0
  25. package/dist/commands/solo-qa.d.ts +6 -0
  26. package/dist/commands/solo-qa.d.ts.map +1 -0
  27. package/dist/commands/solo-qa.js +254 -0
  28. package/dist/commands/solo-qa.js.map +1 -0
  29. package/dist/commands/solo.d.ts +11 -0
  30. package/dist/commands/solo.d.ts.map +1 -0
  31. package/dist/commands/solo.js +43 -0
  32. package/dist/commands/solo.js.map +1 -0
  33. package/dist/index.d.ts +18 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +18 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/lib/artifacts.d.ts +136 -0
  38. package/dist/lib/artifacts.d.ts.map +1 -0
  39. package/dist/lib/artifacts.js +146 -0
  40. package/dist/lib/artifacts.js.map +1 -0
  41. package/dist/lib/doctor.d.ts +45 -0
  42. package/dist/lib/doctor.d.ts.map +1 -0
  43. package/dist/lib/doctor.js +383 -0
  44. package/dist/lib/doctor.js.map +1 -0
  45. package/dist/lib/exec.d.ts +24 -0
  46. package/dist/lib/exec.d.ts.map +1 -0
  47. package/dist/lib/exec.js +295 -0
  48. package/dist/lib/exec.js.map +1 -0
  49. package/dist/lib/formulas.d.ts +78 -0
  50. package/dist/lib/formulas.d.ts.map +1 -0
  51. package/dist/lib/formulas.js +295 -0
  52. package/dist/lib/formulas.js.map +1 -0
  53. package/dist/lib/git.d.ts +9 -0
  54. package/dist/lib/git.d.ts.map +1 -0
  55. package/dist/lib/git.js +60 -0
  56. package/dist/lib/git.js.map +1 -0
  57. package/dist/lib/guidelines.d.ts +43 -0
  58. package/dist/lib/guidelines.d.ts.map +1 -0
  59. package/dist/lib/guidelines.js +195 -0
  60. package/dist/lib/guidelines.js.map +1 -0
  61. package/dist/lib/logger.d.ts +17 -0
  62. package/dist/lib/logger.d.ts.map +1 -0
  63. package/dist/lib/logger.js +42 -0
  64. package/dist/lib/logger.js.map +1 -0
  65. package/dist/lib/retention.d.ts +62 -0
  66. package/dist/lib/retention.d.ts.map +1 -0
  67. package/dist/lib/retention.js +285 -0
  68. package/dist/lib/retention.js.map +1 -0
  69. package/dist/lib/run-history.d.ts +52 -0
  70. package/dist/lib/run-history.d.ts.map +1 -0
  71. package/dist/lib/run-history.js +116 -0
  72. package/dist/lib/run-history.js.map +1 -0
  73. package/dist/lib/run-state.d.ts +58 -0
  74. package/dist/lib/run-state.d.ts.map +1 -0
  75. package/dist/lib/run-state.js +119 -0
  76. package/dist/lib/run-state.js.map +1 -0
  77. package/dist/lib/scope.d.ts +95 -0
  78. package/dist/lib/scope.d.ts.map +1 -0
  79. package/dist/lib/scope.js +291 -0
  80. package/dist/lib/scope.js.map +1 -0
  81. package/dist/lib/selection.d.ts +35 -0
  82. package/dist/lib/selection.d.ts.map +1 -0
  83. package/dist/lib/selection.js +110 -0
  84. package/dist/lib/selection.js.map +1 -0
  85. package/dist/lib/solo-auto.d.ts +87 -0
  86. package/dist/lib/solo-auto.d.ts.map +1 -0
  87. package/dist/lib/solo-auto.js +1230 -0
  88. package/dist/lib/solo-auto.js.map +1 -0
  89. package/dist/lib/solo-ci.d.ts +84 -0
  90. package/dist/lib/solo-ci.d.ts.map +1 -0
  91. package/dist/lib/solo-ci.js +300 -0
  92. package/dist/lib/solo-ci.js.map +1 -0
  93. package/dist/lib/solo-config.d.ts +155 -0
  94. package/dist/lib/solo-config.d.ts.map +1 -0
  95. package/dist/lib/solo-config.js +236 -0
  96. package/dist/lib/solo-config.js.map +1 -0
  97. package/dist/lib/solo-git.d.ts +44 -0
  98. package/dist/lib/solo-git.d.ts.map +1 -0
  99. package/dist/lib/solo-git.js +174 -0
  100. package/dist/lib/solo-git.js.map +1 -0
  101. package/dist/lib/solo-hints.d.ts +32 -0
  102. package/dist/lib/solo-hints.d.ts.map +1 -0
  103. package/dist/lib/solo-hints.js +98 -0
  104. package/dist/lib/solo-hints.js.map +1 -0
  105. package/dist/lib/solo-remote.d.ts +14 -0
  106. package/dist/lib/solo-remote.d.ts.map +1 -0
  107. package/dist/lib/solo-remote.js +48 -0
  108. package/dist/lib/solo-remote.js.map +1 -0
  109. package/dist/lib/solo-stdin.d.ts +13 -0
  110. package/dist/lib/solo-stdin.d.ts.map +1 -0
  111. package/dist/lib/solo-stdin.js +33 -0
  112. package/dist/lib/solo-stdin.js.map +1 -0
  113. package/dist/lib/solo-ticket.d.ts +213 -0
  114. package/dist/lib/solo-ticket.d.ts.map +1 -0
  115. package/dist/lib/solo-ticket.js +850 -0
  116. package/dist/lib/solo-ticket.js.map +1 -0
  117. package/dist/lib/solo-utils.d.ts +133 -0
  118. package/dist/lib/solo-utils.d.ts.map +1 -0
  119. package/dist/lib/solo-utils.js +300 -0
  120. package/dist/lib/solo-utils.js.map +1 -0
  121. package/dist/lib/spindle.d.ts +144 -0
  122. package/dist/lib/spindle.d.ts.map +1 -0
  123. package/dist/lib/spindle.js +388 -0
  124. package/dist/lib/spindle.js.map +1 -0
  125. package/dist/tui/app.d.ts +17 -0
  126. package/dist/tui/app.d.ts.map +1 -0
  127. package/dist/tui/app.js +139 -0
  128. package/dist/tui/app.js.map +1 -0
  129. package/dist/tui/index.d.ts +8 -0
  130. package/dist/tui/index.d.ts.map +1 -0
  131. package/dist/tui/index.js +7 -0
  132. package/dist/tui/index.js.map +1 -0
  133. package/dist/tui/poller.d.ts +42 -0
  134. package/dist/tui/poller.d.ts.map +1 -0
  135. package/dist/tui/poller.js +62 -0
  136. package/dist/tui/poller.js.map +1 -0
  137. package/dist/tui/screens/overview.d.ts +9 -0
  138. package/dist/tui/screens/overview.d.ts.map +1 -0
  139. package/dist/tui/screens/overview.js +189 -0
  140. package/dist/tui/screens/overview.js.map +1 -0
  141. package/dist/tui/state.d.ts +93 -0
  142. package/dist/tui/state.d.ts.map +1 -0
  143. package/dist/tui/state.js +169 -0
  144. package/dist/tui/state.js.map +1 -0
  145. package/dist/tui/types.d.ts +18 -0
  146. package/dist/tui/types.d.ts.map +1 -0
  147. package/dist/tui/types.js +5 -0
  148. package/dist/tui/types.js.map +1 -0
  149. package/package.json +1 -1
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Persistent run state for cross-session cycle tracking.
3
+ *
4
+ * Stored in `.blockspool/run-state.json`. Tracks how many scout cycles
5
+ * have run so periodic tasks (like docs-audit) can trigger automatically.
6
+ */
7
+ import * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ const RUN_STATE_FILE = 'run-state.json';
10
+ function statePath(repoRoot) {
11
+ return path.join(repoRoot, '.blockspool', RUN_STATE_FILE);
12
+ }
13
+ const DEFAULT_STATE = {
14
+ totalCycles: 0,
15
+ lastDocsAuditCycle: 0,
16
+ lastRunAt: 0,
17
+ deferredProposals: [],
18
+ };
19
+ /**
20
+ * Read the current run state from disk.
21
+ */
22
+ export function readRunState(repoRoot) {
23
+ const fp = statePath(repoRoot);
24
+ if (!fs.existsSync(fp))
25
+ return { ...DEFAULT_STATE };
26
+ try {
27
+ const raw = fs.readFileSync(fp, 'utf-8');
28
+ const parsed = JSON.parse(raw);
29
+ return {
30
+ totalCycles: parsed.totalCycles ?? 0,
31
+ lastDocsAuditCycle: parsed.lastDocsAuditCycle ?? 0,
32
+ lastRunAt: parsed.lastRunAt ?? 0,
33
+ deferredProposals: Array.isArray(parsed.deferredProposals) ? parsed.deferredProposals : [],
34
+ };
35
+ }
36
+ catch {
37
+ return { ...DEFAULT_STATE };
38
+ }
39
+ }
40
+ /**
41
+ * Write the run state to disk.
42
+ */
43
+ export function writeRunState(repoRoot, state) {
44
+ const fp = statePath(repoRoot);
45
+ const dir = path.dirname(fp);
46
+ if (!fs.existsSync(dir)) {
47
+ fs.mkdirSync(dir, { recursive: true });
48
+ }
49
+ fs.writeFileSync(fp, JSON.stringify(state, null, 2) + '\n');
50
+ }
51
+ /**
52
+ * Increment the cycle counter and return the new state.
53
+ */
54
+ export function recordCycle(repoRoot) {
55
+ const state = readRunState(repoRoot);
56
+ state.totalCycles += 1;
57
+ state.lastRunAt = Date.now();
58
+ writeRunState(repoRoot, state);
59
+ return state;
60
+ }
61
+ /**
62
+ * Check if a docs-audit cycle is due.
63
+ * Returns true every N cycles since the last docs-audit.
64
+ */
65
+ export function isDocsAuditDue(repoRoot, interval = 3) {
66
+ const state = readRunState(repoRoot);
67
+ return (state.totalCycles - state.lastDocsAuditCycle) >= interval;
68
+ }
69
+ /**
70
+ * Record that a docs-audit was run.
71
+ */
72
+ export function recordDocsAudit(repoRoot) {
73
+ const state = readRunState(repoRoot);
74
+ state.lastDocsAuditCycle = state.totalCycles;
75
+ writeRunState(repoRoot, state);
76
+ }
77
+ /** Max age for deferred proposals (7 days) */
78
+ const MAX_DEFERRED_AGE_MS = 7 * 24 * 60 * 60 * 1000;
79
+ /**
80
+ * Defer a proposal for later when the scope matches.
81
+ */
82
+ export function deferProposal(repoRoot, proposal) {
83
+ const state = readRunState(repoRoot);
84
+ // Avoid duplicates by title
85
+ if (state.deferredProposals.some(d => d.title === proposal.title))
86
+ return;
87
+ state.deferredProposals.push(proposal);
88
+ writeRunState(repoRoot, state);
89
+ }
90
+ /**
91
+ * Retrieve and remove deferred proposals that now match the given scope.
92
+ * Also prunes proposals older than 7 days.
93
+ */
94
+ export function popDeferredForScope(repoRoot, scope) {
95
+ const state = readRunState(repoRoot);
96
+ const now = Date.now();
97
+ const normalizedScope = scope.replace(/\*\*$/, '').replace(/\*$/, '').replace(/\/$/, '');
98
+ const matched = [];
99
+ const remaining = [];
100
+ for (const dp of state.deferredProposals) {
101
+ // Prune stale
102
+ if (now - dp.deferredAt > MAX_DEFERRED_AGE_MS)
103
+ continue;
104
+ const files = dp.files.length > 0 ? dp.files : dp.allowed_paths;
105
+ const inScope = !normalizedScope || files.length === 0 || files.every(f => f.startsWith(normalizedScope) || f.startsWith(normalizedScope + '/'));
106
+ if (inScope) {
107
+ matched.push(dp);
108
+ }
109
+ else {
110
+ remaining.push(dp);
111
+ }
112
+ }
113
+ if (matched.length > 0 || remaining.length !== state.deferredProposals.length) {
114
+ state.deferredProposals = remaining;
115
+ writeRunState(repoRoot, state);
116
+ }
117
+ return matched;
118
+ }
119
+ //# sourceMappingURL=run-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-state.js","sourceRoot":"","sources":["../../src/lib/run-state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Scope enforcement utilities
3
+ *
4
+ * Validates that file changes stay within ticket's allowed paths
5
+ * and don't touch forbidden paths.
6
+ */
7
+ /**
8
+ * Scope violation result
9
+ */
10
+ export interface ScopeViolation {
11
+ file: string;
12
+ violation: 'not_in_allowed' | 'in_forbidden';
13
+ pattern?: string;
14
+ }
15
+ /**
16
+ * Normalize a file path by:
17
+ * - Removing leading ./
18
+ * - Collapsing multiple slashes (//)
19
+ * - Removing trailing slashes (except for root)
20
+ *
21
+ * @param filePath - The file path to normalize
22
+ * @returns Normalized path
23
+ */
24
+ export declare function normalizePath(filePath: string): string;
25
+ /**
26
+ * Detect if a path appears to be hallucinated by Claude.
27
+ * Common hallucination patterns include repeated path segments like 'foo/foo/'.
28
+ *
29
+ * @param filePath - The file path to check
30
+ * @returns Object with isHallucinated flag and optional reason
31
+ */
32
+ export declare function detectHallucinatedPath(filePath: string): {
33
+ isHallucinated: boolean;
34
+ reason?: string;
35
+ };
36
+ /**
37
+ * Check if changed files violate ticket scope constraints
38
+ *
39
+ * @param changedFiles - List of file paths that were modified
40
+ * @param allowedPaths - Glob patterns for allowed paths (empty = all allowed)
41
+ * @param forbiddenPaths - Glob patterns for forbidden paths
42
+ * @returns List of violations, empty if all files are within scope
43
+ */
44
+ export declare function checkScopeViolations(changedFiles: string[], allowedPaths: string[], forbiddenPaths: string[]): ScopeViolation[];
45
+ /**
46
+ * Simple glob-style pattern matching
47
+ * Supports: * (any chars), ** (any path segments), ? (single char)
48
+ *
49
+ * @param filePath - The file path to check
50
+ * @param pattern - Glob pattern to match against
51
+ * @returns true if the file matches the pattern
52
+ */
53
+ export declare function matchesPattern(filePath: string, pattern: string): boolean;
54
+ /**
55
+ * Result of analyzing scope violations for auto-expansion
56
+ */
57
+ export interface ScopeExpansionResult {
58
+ /** Whether the paths can be expanded to fix violations */
59
+ canExpand: boolean;
60
+ /** New allowed paths after expansion */
61
+ expandedPaths: string[];
62
+ /** Files that were added to allowed paths */
63
+ addedPaths: string[];
64
+ /** Reason if expansion is not possible */
65
+ reason?: string;
66
+ }
67
+ /**
68
+ * Analyze scope violations and determine if paths can be auto-expanded.
69
+ *
70
+ * This enables "organic" handling of blocked tickets by automatically
71
+ * expanding allowed_paths for reasonable related files:
72
+ * - Same directory (sibling files)
73
+ * - Test files for source files
74
+ * - Type definition files (.d.ts)
75
+ * - Index files
76
+ *
77
+ * Will NOT expand for:
78
+ * - Hallucinated paths
79
+ * - Forbidden path violations
80
+ * - Files in completely unrelated directories
81
+ *
82
+ * @param violations - List of scope violations from checkScopeViolations
83
+ * @param currentAllowedPaths - Current allowed_paths for the ticket
84
+ * @param maxExpansions - Maximum number of paths to add (default: 10)
85
+ * @returns Expansion result with new paths or reason for rejection
86
+ */
87
+ export declare function analyzeViolationsForExpansion(violations: ScopeViolation[], currentAllowedPaths: string[], maxExpansions?: number): ScopeExpansionResult;
88
+ /**
89
+ * Parse git status --porcelain output to get changed file paths
90
+ *
91
+ * @param statusOutput - Output from `git status --porcelain`
92
+ * @returns List of file paths that were changed
93
+ */
94
+ export declare function parseChangedFiles(statusOutput: string): string[];
95
+ //# sourceMappingURL=scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../src/lib/scope.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,gBAAgB,GAAG,cAAc,CAAC;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMtD;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG;IACxD,cAAc,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAwBA;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EAAE,EACtB,YAAY,EAAE,MAAM,EAAE,EACtB,cAAc,EAAE,MAAM,EAAE,GACvB,cAAc,EAAE,CAkDlB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAyBzE;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,SAAS,EAAE,OAAO,CAAC;IACnB,wCAAwC;IACxC,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,6BAA6B,CAC3C,UAAU,EAAE,cAAc,EAAE,EAC5B,mBAAmB,EAAE,MAAM,EAAE,EAC7B,aAAa,GAAE,MAAW,GACzB,oBAAoB,CAyItB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAehE"}
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Scope enforcement utilities
3
+ *
4
+ * Validates that file changes stay within ticket's allowed paths
5
+ * and don't touch forbidden paths.
6
+ */
7
+ /**
8
+ * Normalize a file path by:
9
+ * - Removing leading ./
10
+ * - Collapsing multiple slashes (//)
11
+ * - Removing trailing slashes (except for root)
12
+ *
13
+ * @param filePath - The file path to normalize
14
+ * @returns Normalized path
15
+ */
16
+ export function normalizePath(filePath) {
17
+ return filePath
18
+ .replace(/\\/g, '/') // Normalize backslashes
19
+ .replace(/^\.\//, '') // Remove leading ./
20
+ .replace(/\/+/g, '/') // Collapse multiple slashes
21
+ .replace(/\/$/, ''); // Remove trailing slash
22
+ }
23
+ /**
24
+ * Detect if a path appears to be hallucinated by Claude.
25
+ * Common hallucination patterns include repeated path segments like 'foo/foo/'.
26
+ *
27
+ * @param filePath - The file path to check
28
+ * @returns Object with isHallucinated flag and optional reason
29
+ */
30
+ export function detectHallucinatedPath(filePath) {
31
+ const normalized = normalizePath(filePath);
32
+ const segments = normalized.split('/').filter(s => s.length > 0);
33
+ // Check for repeated consecutive segments (e.g., 'cloud/cloud/', 'src/src/')
34
+ for (let i = 0; i < segments.length - 1; i++) {
35
+ if (segments[i] === segments[i + 1] && segments[i].length > 0) {
36
+ return {
37
+ isHallucinated: true,
38
+ reason: `Repeated path segment: '${segments[i]}/${segments[i]}'`,
39
+ };
40
+ }
41
+ }
42
+ // Check for obviously malformed patterns
43
+ if (/\/\//.test(filePath)) {
44
+ // Double slashes in original (before normalization)
45
+ return {
46
+ isHallucinated: true,
47
+ reason: 'Contains double slashes',
48
+ };
49
+ }
50
+ return { isHallucinated: false };
51
+ }
52
+ /**
53
+ * Check if changed files violate ticket scope constraints
54
+ *
55
+ * @param changedFiles - List of file paths that were modified
56
+ * @param allowedPaths - Glob patterns for allowed paths (empty = all allowed)
57
+ * @param forbiddenPaths - Glob patterns for forbidden paths
58
+ * @returns List of violations, empty if all files are within scope
59
+ */
60
+ export function checkScopeViolations(changedFiles, allowedPaths, forbiddenPaths) {
61
+ const violations = [];
62
+ for (const file of changedFiles) {
63
+ // Check for hallucinated paths first
64
+ const hallucinationCheck = detectHallucinatedPath(file);
65
+ if (hallucinationCheck.isHallucinated) {
66
+ console.warn(`[scope] Warning: Detected hallucinated path '${file}': ${hallucinationCheck.reason}`);
67
+ // Report hallucinated paths as not_in_allowed since they are invalid
68
+ violations.push({
69
+ file,
70
+ violation: 'not_in_allowed',
71
+ pattern: `hallucinated: ${hallucinationCheck.reason}`,
72
+ });
73
+ continue; // Skip further checks for hallucinated paths
74
+ }
75
+ // Normalize the path for consistent matching
76
+ const normalizedFile = normalizePath(file);
77
+ // Check forbidden paths first (higher priority)
78
+ for (const pattern of forbiddenPaths) {
79
+ if (matchesPattern(normalizedFile, pattern)) {
80
+ violations.push({ file, violation: 'in_forbidden', pattern });
81
+ break; // Don't check allowed if forbidden
82
+ }
83
+ }
84
+ // If no forbidden match and allowed_paths is specified, check allowed
85
+ if (allowedPaths.length > 0 && !violations.some(v => v.file === file)) {
86
+ let isAllowed = allowedPaths.some(pattern => matchesPattern(normalizedFile, pattern));
87
+ // For directory entries (git shows new dirs with trailing /),
88
+ // check if any allowed path is under this directory
89
+ if (!isAllowed && file.endsWith('/')) {
90
+ const dirPrefix = normalizedFile + '/';
91
+ isAllowed = allowedPaths.some(pattern => pattern.startsWith(dirPrefix));
92
+ }
93
+ if (!isAllowed) {
94
+ violations.push({ file, violation: 'not_in_allowed' });
95
+ }
96
+ }
97
+ }
98
+ return violations;
99
+ }
100
+ /**
101
+ * Simple glob-style pattern matching
102
+ * Supports: * (any chars), ** (any path segments), ? (single char)
103
+ *
104
+ * @param filePath - The file path to check
105
+ * @param pattern - Glob pattern to match against
106
+ * @returns true if the file matches the pattern
107
+ */
108
+ export function matchesPattern(filePath, pattern) {
109
+ // Normalize paths
110
+ const normalizedFile = filePath.replace(/\\/g, '/');
111
+ const normalizedPattern = pattern.replace(/\\/g, '/');
112
+ // Convert glob to regex
113
+ // Step 1: Replace glob patterns with placeholders
114
+ let regexPattern = normalizedPattern
115
+ .replace(/\*\*\//g, '<<<GLOBSTAR_SLASH>>>') // **/ matches zero or more directories
116
+ .replace(/\*\*/g, '<<<GLOBSTAR>>>') // ** at end matches anything
117
+ .replace(/\*/g, '<<<STAR>>>') // * matches anything except /
118
+ .replace(/\?/g, '<<<QUESTION>>>'); // ? matches single char
119
+ // Step 2: Escape special regex chars (but not our placeholders)
120
+ regexPattern = regexPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
121
+ // Step 3: Replace placeholders with regex patterns
122
+ regexPattern = regexPattern
123
+ .replace(/<<<GLOBSTAR_SLASH>>>/g, '(?:.*/)?') // **/ = zero or more dirs
124
+ .replace(/<<<GLOBSTAR>>>/g, '.*') // ** = anything
125
+ .replace(/<<<STAR>>>/g, '[^/]*') // * = anything except /
126
+ .replace(/<<<QUESTION>>>/g, '[^/]'); // ? = single char except /
127
+ const regex = new RegExp(`^${regexPattern}$`);
128
+ return regex.test(normalizedFile);
129
+ }
130
+ /**
131
+ * Analyze scope violations and determine if paths can be auto-expanded.
132
+ *
133
+ * This enables "organic" handling of blocked tickets by automatically
134
+ * expanding allowed_paths for reasonable related files:
135
+ * - Same directory (sibling files)
136
+ * - Test files for source files
137
+ * - Type definition files (.d.ts)
138
+ * - Index files
139
+ *
140
+ * Will NOT expand for:
141
+ * - Hallucinated paths
142
+ * - Forbidden path violations
143
+ * - Files in completely unrelated directories
144
+ *
145
+ * @param violations - List of scope violations from checkScopeViolations
146
+ * @param currentAllowedPaths - Current allowed_paths for the ticket
147
+ * @param maxExpansions - Maximum number of paths to add (default: 10)
148
+ * @returns Expansion result with new paths or reason for rejection
149
+ */
150
+ export function analyzeViolationsForExpansion(violations, currentAllowedPaths, maxExpansions = 10) {
151
+ // Check for forbidden violations - these cannot be expanded
152
+ const forbiddenViolations = violations.filter(v => v.violation === 'in_forbidden');
153
+ if (forbiddenViolations.length > 0) {
154
+ return {
155
+ canExpand: false,
156
+ expandedPaths: currentAllowedPaths,
157
+ addedPaths: [],
158
+ reason: `Cannot expand: ${forbiddenViolations.length} file(s) match forbidden paths`,
159
+ };
160
+ }
161
+ // Check for hallucinated paths - these cannot be expanded
162
+ const hallucinatedViolations = violations.filter(v => v.pattern?.startsWith('hallucinated:'));
163
+ if (hallucinatedViolations.length > 0) {
164
+ return {
165
+ canExpand: false,
166
+ expandedPaths: currentAllowedPaths,
167
+ addedPaths: [],
168
+ reason: `Cannot expand: ${hallucinatedViolations.length} hallucinated path(s) detected`,
169
+ };
170
+ }
171
+ // Get the "allowed" directories from current paths
172
+ const allowedDirs = new Set();
173
+ for (const path of currentAllowedPaths) {
174
+ const normalized = normalizePath(path);
175
+ const lastSlash = normalized.lastIndexOf('/');
176
+ if (lastSlash > 0) {
177
+ allowedDirs.add(normalized.slice(0, lastSlash));
178
+ }
179
+ }
180
+ // Analyze each violation and check if it's a "reasonable" expansion
181
+ const pathsToAdd = [];
182
+ const notAllowedViolations = violations.filter(v => v.violation === 'not_in_allowed');
183
+ for (const violation of notAllowedViolations) {
184
+ const normalizedFile = normalizePath(violation.file);
185
+ const lastSlash = normalizedFile.lastIndexOf('/');
186
+ const fileDir = lastSlash > 0 ? normalizedFile.slice(0, lastSlash) : '';
187
+ const fileName = lastSlash > 0 ? normalizedFile.slice(lastSlash + 1) : normalizedFile;
188
+ // Check if file is in a directory we already allow (sibling file)
189
+ const isSiblingFile = allowedDirs.has(fileDir);
190
+ // Check if file is a related type (types file, test file, index file)
191
+ const isRelatedFile = fileName.endsWith('.d.ts') ||
192
+ fileName.includes('.test.') ||
193
+ fileName.includes('.spec.') ||
194
+ fileName === 'index.ts' ||
195
+ fileName === 'index.tsx' ||
196
+ fileName === 'index.js' ||
197
+ fileName === 'types.ts' ||
198
+ fileName === 'types.tsx';
199
+ // Check if file is in an allowed subdirectory
200
+ const isSubdirectory = [...allowedDirs].some(dir => fileDir.startsWith(dir + '/'));
201
+ // Check if file is a root-level config (vitest.config.ts, tsconfig.json, etc.)
202
+ const isRootConfig = !fileDir && /^(vitest|vite|jest|tsconfig|eslint|prettier|babel|rollup|webpack|next|tailwind)\b/.test(fileName);
203
+ if (isSiblingFile || isRelatedFile || isSubdirectory || isRootConfig) {
204
+ pathsToAdd.push(normalizedFile);
205
+ }
206
+ }
207
+ // Second pass: if some violations remain, check if they share a common
208
+ // top-level module with the allowed paths (e.g. both under src/api/).
209
+ // This handles legitimate cross-directory refactors like renames.
210
+ if (pathsToAdd.length < notAllowedViolations.length) {
211
+ const allowedTopDirs = new Set();
212
+ for (const dir of allowedDirs) {
213
+ const parts = dir.split('/');
214
+ // Add the root directory (e.g. "src" from "src/lib/utils")
215
+ if (parts.length >= 1) {
216
+ allowedTopDirs.add(parts[0]);
217
+ }
218
+ // Also add first two segments as the "module" (e.g. "src/lib")
219
+ if (parts.length >= 2) {
220
+ allowedTopDirs.add(parts.slice(0, 2).join('/'));
221
+ }
222
+ }
223
+ // Cross-package awareness: if any packages/*/src path is allowed,
224
+ // treat other packages/*/src paths as related (monorepo siblings)
225
+ const hasPackagesDir = [...allowedDirs].some(d => d.startsWith('packages/'));
226
+ if (hasPackagesDir) {
227
+ allowedTopDirs.add('packages');
228
+ }
229
+ for (const violation of notAllowedViolations) {
230
+ const normalizedFile = normalizePath(violation.file);
231
+ if (pathsToAdd.includes(normalizedFile))
232
+ continue;
233
+ const fileParts = normalizedFile.split('/');
234
+ const fileTopDir = fileParts.length >= 2
235
+ ? fileParts.slice(0, 2).join('/')
236
+ : fileParts[0];
237
+ if (allowedTopDirs.has(fileTopDir) || allowedTopDirs.has(fileParts[0])) {
238
+ pathsToAdd.push(normalizedFile);
239
+ }
240
+ }
241
+ }
242
+ // Check if we have too many expansions
243
+ if (pathsToAdd.length === 0) {
244
+ return {
245
+ canExpand: false,
246
+ expandedPaths: currentAllowedPaths,
247
+ addedPaths: [],
248
+ reason: `Cannot expand: ${notAllowedViolations.length} file(s) in unrelated directories`,
249
+ };
250
+ }
251
+ if (pathsToAdd.length > maxExpansions) {
252
+ return {
253
+ canExpand: false,
254
+ expandedPaths: currentAllowedPaths,
255
+ addedPaths: [],
256
+ reason: `Cannot expand: ${pathsToAdd.length} files need expansion (max: ${maxExpansions})`,
257
+ };
258
+ }
259
+ // Merge new paths with existing
260
+ const expandedPaths = [...currentAllowedPaths, ...pathsToAdd];
261
+ const uniquePaths = [...new Set(expandedPaths)];
262
+ return {
263
+ canExpand: true,
264
+ expandedPaths: uniquePaths,
265
+ addedPaths: pathsToAdd,
266
+ };
267
+ }
268
+ /**
269
+ * Parse git status --porcelain output to get changed file paths
270
+ *
271
+ * @param statusOutput - Output from `git status --porcelain`
272
+ * @returns List of file paths that were changed
273
+ */
274
+ export function parseChangedFiles(statusOutput) {
275
+ if (!statusOutput.trim())
276
+ return [];
277
+ return statusOutput
278
+ .split('\n')
279
+ .filter(line => line.trim())
280
+ .map(line => {
281
+ // Format: XY filename or XY original -> renamed
282
+ // eslint-disable-next-line security/detect-unsafe-regex
283
+ const match = line.match(/^..\s+(.+?)(?:\s+->\s+(.+))?$/);
284
+ if (!match)
285
+ return null;
286
+ // Return the destination for renames, otherwise the filename
287
+ return match[2] || match[1];
288
+ })
289
+ .filter((f) => f !== null);
290
+ }
291
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.js","sourceRoot":"","sources":["../../src/lib/scope.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Selection parser for user input
3
+ *
4
+ * Parses selection strings like:
5
+ * - "1" → [0]
6
+ * - "1,3,5" → [0, 2, 4]
7
+ * - "1-3" → [0, 1, 2]
8
+ * - "1-3,5,7" → [0, 1, 2, 4, 6]
9
+ * - "all" → all indices
10
+ *
11
+ * Note: User input is 1-indexed, output is 0-indexed.
12
+ */
13
+ /**
14
+ * Parse a selection string into 0-indexed array indices
15
+ *
16
+ * @param input - Selection string (e.g., "1-3,5,7" or "all")
17
+ * @param maxIndex - Maximum valid index (0-indexed, exclusive)
18
+ * @returns Array of 0-indexed indices, sorted and deduplicated
19
+ * @throws Error for invalid input
20
+ */
21
+ export declare function parseSelection(input: string, maxIndex: number): number[];
22
+ /**
23
+ * Validate a selection string without parsing
24
+ *
25
+ * @returns true if valid, false otherwise
26
+ */
27
+ export declare function isValidSelection(input: string, maxIndex: number): boolean;
28
+ /**
29
+ * Format a selection for display (1-indexed)
30
+ *
31
+ * @param indices - 0-indexed array indices
32
+ * @returns Human-readable string like "1, 2, 3" or "1-3"
33
+ */
34
+ export declare function formatSelection(indices: number[]): string;
35
+ //# sourceMappingURL=selection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selection.d.ts","sourceRoot":"","sources":["../../src/lib/selection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAoDxE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAOzE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CA0BzD"}