specsmd 0.1.61 → 0.1.63

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.
@@ -704,7 +704,7 @@ function printUsage() {
704
704
  console.error('');
705
705
  console.error('Arguments:');
706
706
  console.error(' rootPath - Project root directory');
707
- console.error(' runId - Run ID to complete (e.g., run-003)');
707
+ console.error(' runId - Run ID to complete (e.g., run-fabriqa-2026-003)');
708
708
  console.error('');
709
709
  console.error('Flags:');
710
710
  console.error(' --complete-item - Complete only the current work item (batch/wide runs)');
@@ -718,8 +718,8 @@ function printUsage() {
718
718
  console.error(' --coverage=N - Coverage percentage');
719
719
  console.error('');
720
720
  console.error('Examples:');
721
- console.error(' node complete-run.cjs /project run-003 --complete-item');
722
- console.error(' node complete-run.cjs /project run-003 --complete-run --tests=5 --coverage=85');
721
+ console.error(' node complete-run.cjs /project run-fabriqa-2026-003 --complete-item');
722
+ console.error(' node complete-run.cjs /project run-fabriqa-2026-003 --complete-run --tests=5 --coverage=85');
723
723
  }
724
724
 
725
725
  // =============================================================================
@@ -152,26 +152,61 @@ function writeState(statePath, state) {
152
152
  }
153
153
 
154
154
  // =============================================================================
155
- // Run ID Generation (CRITICAL - checks both history and file system)
155
+ // Run ID Generation (CRITICAL - checks state and file system)
156
156
  // =============================================================================
157
157
 
158
- function generateRunId(runsPath, state) {
158
+ function sanitizeWorktreeToken(value) {
159
+ const normalized = String(value || '')
160
+ .toLowerCase()
161
+ .replace(/[^a-z0-9]+/g, '-')
162
+ .replace(/^-+|-+$/g, '');
163
+ return normalized || 'workspace';
164
+ }
165
+
166
+ function resolveWorktreeToken(rootPath) {
167
+ const baseName = path.basename(path.resolve(String(rootPath || '')));
168
+ return sanitizeWorktreeToken(baseName);
169
+ }
170
+
171
+ function parseRunSequence(runId, worktreeToken) {
172
+ if (typeof runId !== 'string' || runId.trim() === '') {
173
+ return null;
174
+ }
175
+
176
+ const legacyMatch = runId.match(/^run-(\d+)$/);
177
+ if (legacyMatch) {
178
+ const parsed = parseInt(legacyMatch[1], 10);
179
+ return Number.isFinite(parsed) ? parsed : null;
180
+ }
181
+
182
+ const worktreeMatch = runId.match(/^run-([a-z0-9][a-z0-9-]*)-(\d+)$/);
183
+ if (worktreeMatch && worktreeMatch[1] === worktreeToken) {
184
+ const parsed = parseInt(worktreeMatch[2], 10);
185
+ return Number.isFinite(parsed) ? parsed : null;
186
+ }
187
+
188
+ return null;
189
+ }
190
+
191
+ function generateRunId(rootPath, runsPath, state) {
159
192
  // Ensure runs directory exists
160
193
  if (!fs.existsSync(runsPath)) {
161
194
  fs.mkdirSync(runsPath, { recursive: true });
162
195
  }
163
196
 
164
- // Source 1: Get max from state.yaml runs.completed history
165
- let maxFromHistory = 0;
166
- if (state.runs && Array.isArray(state.runs.completed)) {
167
- for (const run of state.runs.completed) {
168
- if (run.id) {
169
- const match = run.id.match(/^run-(\d+)$/);
170
- if (match) {
171
- const num = parseInt(match[1], 10);
172
- if (num > maxFromHistory) maxFromHistory = num;
173
- }
174
- }
197
+ const worktreeToken = resolveWorktreeToken(rootPath);
198
+ let maxFromState = 0;
199
+
200
+ // Source 1: Get max from state.yaml run history (active + completed)
201
+ const stateRuns = state?.runs || {};
202
+ const stateRunRecords = [
203
+ ...(Array.isArray(stateRuns.active) ? stateRuns.active : []),
204
+ ...(Array.isArray(stateRuns.completed) ? stateRuns.completed : [])
205
+ ];
206
+ for (const run of stateRunRecords) {
207
+ const num = parseRunSequence(run?.id, worktreeToken);
208
+ if (num != null && num > maxFromState) {
209
+ maxFromState = num;
175
210
  }
176
211
  }
177
212
 
@@ -180,9 +215,9 @@ function generateRunId(runsPath, state) {
180
215
  try {
181
216
  const entries = fs.readdirSync(runsPath);
182
217
  for (const entry of entries) {
183
- if (/^run-\d{3,}$/.test(entry)) {
184
- const num = parseInt(entry.replace('run-', ''), 10);
185
- if (num > maxFromFileSystem) maxFromFileSystem = num;
218
+ const num = parseRunSequence(entry, worktreeToken);
219
+ if (num != null && num > maxFromFileSystem) {
220
+ maxFromFileSystem = num;
186
221
  }
187
222
  }
188
223
  } catch (err) {
@@ -194,10 +229,10 @@ function generateRunId(runsPath, state) {
194
229
  }
195
230
 
196
231
  // Use MAX of both to ensure no duplicates
197
- const maxNum = Math.max(maxFromHistory, maxFromFileSystem);
232
+ const maxNum = Math.max(maxFromState, maxFromFileSystem);
198
233
  const nextNum = maxNum + 1;
199
234
 
200
- return `run-${String(nextNum).padStart(3, '0')}`;
235
+ return `run-${worktreeToken}-${String(nextNum).padStart(3, '0')}`;
201
236
  }
202
237
 
203
238
  // =============================================================================
@@ -333,7 +368,7 @@ function initRun(rootPath, workItems, scope) {
333
368
  }
334
369
 
335
370
  // Generate run ID (checks both history AND file system)
336
- const runId = generateRunId(runsPath, state);
371
+ const runId = generateRunId(rootPath, runsPath, state);
337
372
  const runPath = path.join(runsPath, runId);
338
373
 
339
374
  // Create run folder
@@ -9,8 +9,8 @@
9
9
  * node update-checkpoint.cjs <rootPath> <runId> <checkpointState> [--item=<workItemId>] [--checkpoint=<name>]
10
10
  *
11
11
  * Examples:
12
- * node update-checkpoint.cjs /project run-001 awaiting_approval --checkpoint=plan
13
- * node update-checkpoint.cjs /project run-001 approved
12
+ * node update-checkpoint.cjs /project run-fabriqa-2026-001 awaiting_approval --checkpoint=plan
13
+ * node update-checkpoint.cjs /project run-fabriqa-2026-001 approved
14
14
  */
15
15
 
16
16
  const fs = require('fs');
@@ -223,8 +223,8 @@ function printUsage() {
223
223
  console.error(` ${VALID_STATES.join(', ')}`);
224
224
  console.error('');
225
225
  console.error('Examples:');
226
- console.error(' node update-checkpoint.cjs /project run-001 awaiting_approval --checkpoint=plan');
227
- console.error(' node update-checkpoint.cjs /project run-001 approved');
226
+ console.error(' node update-checkpoint.cjs /project run-fabriqa-2026-001 awaiting_approval --checkpoint=plan');
227
+ console.error(' node update-checkpoint.cjs /project run-fabriqa-2026-001 approved');
228
228
  }
229
229
 
230
230
  if (require.main === module) {
@@ -10,9 +10,9 @@
10
10
  * node update-phase.cjs <rootPath> <runId> <phase>
11
11
  *
12
12
  * Examples:
13
- * node update-phase.cjs /project run-001 execute
14
- * node update-phase.cjs /project run-001 test
15
- * node update-phase.cjs /project run-001 review
13
+ * node update-phase.cjs /project run-fabriqa-2026-001 execute
14
+ * node update-phase.cjs /project run-fabriqa-2026-001 test
15
+ * node update-phase.cjs /project run-fabriqa-2026-001 review
16
16
  */
17
17
 
18
18
  const fs = require('fs');
@@ -219,12 +219,12 @@ function printUsage() {
219
219
  console.error('');
220
220
  console.error('Arguments:');
221
221
  console.error(' rootPath - Project root directory');
222
- console.error(' runId - Run ID (e.g., run-001)');
222
+ console.error(' runId - Run ID (e.g., run-fabriqa-2026-001)');
223
223
  console.error(' phase - New phase: plan, execute, test, review');
224
224
  console.error('');
225
225
  console.error('Examples:');
226
- console.error(' node update-phase.cjs /project run-001 execute');
227
- console.error(' node update-phase.cjs /project run-001 test');
226
+ console.error(' node update-phase.cjs /project run-fabriqa-2026-001 execute');
227
+ console.error(' node update-phase.cjs /project run-fabriqa-2026-001 test');
228
228
  }
229
229
 
230
230
  if (require.main === module) {
@@ -1127,12 +1127,6 @@ function createDashboardApp(deps) {
1127
1127
  columns: Math.max(1, stdout.columns || process.stdout.columns || 120),
1128
1128
  rows: Math.max(1, stdout.rows || process.stdout.rows || 40)
1129
1129
  });
1130
-
1131
- // Resize in some terminals can leave stale frame rows behind.
1132
- // Keep the clear operation minimal to avoid triggering scrollback churn.
1133
- if (typeof stdout.write === 'function' && stdout.isTTY !== false) {
1134
- stdout.write('\u001B[H\u001B[J');
1135
- }
1136
1130
  };
1137
1131
 
1138
1132
  updateSize();
@@ -6,6 +6,73 @@ const {
6
6
  loadGitCommitPreview
7
7
  } = require('../git/changes');
8
8
 
9
+ const MAX_PREVIEW_CACHE_ENTRIES = 64;
10
+ const previewContentCache = new Map();
11
+
12
+ function getFilePreviewCacheKey(filePath) {
13
+ if (typeof filePath !== 'string' || filePath.trim() === '') {
14
+ return null;
15
+ }
16
+
17
+ try {
18
+ const stat = fs.statSync(filePath);
19
+ return `file:${filePath}:${stat.size}:${Math.floor(stat.mtimeMs)}`;
20
+ } catch {
21
+ return `file:${filePath}:missing`;
22
+ }
23
+ }
24
+
25
+ function getGitPreviewCacheKey(fileEntry, isGitCommitPreview) {
26
+ if (!fileEntry || typeof fileEntry !== 'object') {
27
+ return null;
28
+ }
29
+
30
+ const repoRoot = typeof fileEntry.repoRoot === 'string' ? fileEntry.repoRoot : '';
31
+ if (isGitCommitPreview) {
32
+ const commitHash = typeof fileEntry.commitHash === 'string' ? fileEntry.commitHash : '';
33
+ return `git-commit:${repoRoot}:${commitHash}`;
34
+ }
35
+
36
+ const bucket = typeof fileEntry.bucket === 'string' ? fileEntry.bucket : '';
37
+ const relativePath = typeof fileEntry.relativePath === 'string' ? fileEntry.relativePath : '';
38
+ const pathValue = typeof fileEntry.path === 'string' ? fileEntry.path : '';
39
+ return `git-diff:${repoRoot}:${bucket}:${relativePath}:${pathValue}`;
40
+ }
41
+
42
+ function getCachedPreviewContent(cacheKey) {
43
+ if (!cacheKey || !previewContentCache.has(cacheKey)) {
44
+ return null;
45
+ }
46
+
47
+ const cached = previewContentCache.get(cacheKey);
48
+ previewContentCache.delete(cacheKey);
49
+ previewContentCache.set(cacheKey, cached);
50
+ return Array.isArray(cached) ? cached : null;
51
+ }
52
+
53
+ function setCachedPreviewContent(cacheKey, lines) {
54
+ if (!cacheKey || !Array.isArray(lines)) {
55
+ return;
56
+ }
57
+
58
+ if (previewContentCache.has(cacheKey)) {
59
+ previewContentCache.delete(cacheKey);
60
+ }
61
+ previewContentCache.set(cacheKey, lines);
62
+
63
+ while (previewContentCache.size > MAX_PREVIEW_CACHE_ENTRIES) {
64
+ const oldest = previewContentCache.keys().next().value;
65
+ if (!oldest) {
66
+ break;
67
+ }
68
+ previewContentCache.delete(oldest);
69
+ }
70
+ }
71
+
72
+ function clearPreviewContentCache() {
73
+ previewContentCache.clear();
74
+ }
75
+
9
76
  function buildPreviewLines(fileEntry, width, scrollOffset, options = {}) {
10
77
  const fullDocument = options?.fullDocument === true;
11
78
 
@@ -16,24 +83,31 @@ function buildPreviewLines(fileEntry, width, scrollOffset, options = {}) {
16
83
  const isGitFilePreview = fileEntry.previewType === 'git-diff';
17
84
  const isGitCommitPreview = fileEntry.previewType === 'git-commit-diff';
18
85
  const isGitPreview = isGitFilePreview || isGitCommitPreview;
19
- let rawLines = [];
20
- if (isGitPreview) {
21
- const diffText = isGitCommitPreview
22
- ? loadGitCommitPreview(fileEntry)
23
- : loadGitDiffPreview(fileEntry);
24
- rawLines = String(diffText || '').split(/\r?\n/);
25
- } else {
26
- let content;
27
- try {
28
- content = fs.readFileSync(fileEntry.path, 'utf8');
29
- } catch (error) {
30
- return [{
31
- text: truncate(`Unable to read ${fileEntry.label || fileEntry.path}: ${error.message}`, width),
32
- color: 'red',
33
- bold: false
34
- }];
86
+ const cacheKey = isGitPreview
87
+ ? getGitPreviewCacheKey(fileEntry, isGitCommitPreview)
88
+ : getFilePreviewCacheKey(fileEntry.path);
89
+ let rawLines = getCachedPreviewContent(cacheKey);
90
+ if (!rawLines) {
91
+ if (isGitPreview) {
92
+ const diffText = isGitCommitPreview
93
+ ? loadGitCommitPreview(fileEntry)
94
+ : loadGitDiffPreview(fileEntry);
95
+ rawLines = String(diffText || '').split(/\r?\n/);
96
+ } else {
97
+ let content;
98
+ try {
99
+ content = fs.readFileSync(fileEntry.path, 'utf8');
100
+ } catch (error) {
101
+ return [{
102
+ text: truncate(`Unable to read ${fileEntry.label || fileEntry.path}: ${error.message}`, width),
103
+ color: 'red',
104
+ bold: false
105
+ }];
106
+ }
107
+ rawLines = String(content).split(/\r?\n/);
35
108
  }
36
- rawLines = String(content).split(/\r?\n/);
109
+
110
+ setCachedPreviewContent(cacheKey, rawLines);
37
111
  }
38
112
 
39
113
  const headLine = {
@@ -141,5 +215,6 @@ function allocateSingleColumnPanels(candidates, rowsBudget) {
141
215
 
142
216
  module.exports = {
143
217
  buildPreviewLines,
144
- allocateSingleColumnPanels
218
+ allocateSingleColumnPanels,
219
+ clearPreviewContentCache
145
220
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.61",
3
+ "version": "0.1.63",
4
4
  "description": "Multi-agent orchestration system for AI-native software development. Delivers AI-DLC, Agile, and custom SDLC flows as markdown-based agent systems.",
5
5
  "main": "lib/installer.js",
6
6
  "bin": {