specsmd 0.1.70 → 0.1.71

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.
@@ -114,6 +114,25 @@ function normalizeTimestamp(value) {
114
114
  return parsed.toISOString();
115
115
  }
116
116
 
117
+ function compareByCreatedAtThenId(a, b) {
118
+ const aTime = a?.createdAt ? Date.parse(a.createdAt) : NaN;
119
+ const bTime = b?.createdAt ? Date.parse(b.createdAt) : NaN;
120
+ const aHasTime = !Number.isNaN(aTime);
121
+ const bHasTime = !Number.isNaN(bTime);
122
+
123
+ if (aHasTime && bHasTime && aTime !== bTime) {
124
+ return aTime - bTime;
125
+ }
126
+ if (aHasTime && !bHasTime) {
127
+ return -1;
128
+ }
129
+ if (!aHasTime && bHasTime) {
130
+ return 1;
131
+ }
132
+
133
+ return String(a?.id || '').localeCompare(String(b?.id || ''));
134
+ }
135
+
117
136
  function parseIntentFolderName(folderName) {
118
137
  const match = String(folderName).match(/^(\d{3})-(.+)$/);
119
138
  if (!match) {
@@ -272,7 +291,8 @@ function parseIntent(intentPath, warnings) {
272
291
  completedStories: storyStats.completed,
273
292
  inProgressStories: storyStats.inProgress,
274
293
  pendingStories: storyStats.pending,
275
- blockedStories: storyStats.blocked
294
+ blockedStories: storyStats.blocked,
295
+ createdAt: normalizeTimestamp(requirementsFrontmatter.created)
276
296
  };
277
297
  }
278
298
 
@@ -486,7 +506,7 @@ function parseAidlcDashboard(workspacePath) {
486
506
  const intents = intentFolders
487
507
  .map((intentFolder) => parseIntent(path.join(intentsPath, intentFolder), warnings))
488
508
  .filter(Boolean)
489
- .sort((a, b) => a.id.localeCompare(b.id));
509
+ .sort(compareByCreatedAtThenId);
490
510
 
491
511
  if (intentFolders.length === 0) {
492
512
  warnings.push('No intents found under memory-bank/intents.');
@@ -350,7 +350,8 @@ function buildPendingItems(intents) {
350
350
  mode: item.mode,
351
351
  complexity: item.complexity,
352
352
  dependencies: item.dependencies || [],
353
- filePath: item.filePath
353
+ filePath: item.filePath,
354
+ createdAt: item.createdAt
354
355
  });
355
356
  }
356
357
  }
@@ -360,6 +361,19 @@ function buildPendingItems(intents) {
360
361
  if (depDiff !== 0) {
361
362
  return depDiff;
362
363
  }
364
+ const aTime = a.createdAt ? Date.parse(a.createdAt) : NaN;
365
+ const bTime = b.createdAt ? Date.parse(b.createdAt) : NaN;
366
+ const aHasTime = !Number.isNaN(aTime);
367
+ const bHasTime = !Number.isNaN(bTime);
368
+ if (aHasTime && bHasTime && aTime !== bTime) {
369
+ return aTime - bTime;
370
+ }
371
+ if (aHasTime && !bHasTime) {
372
+ return -1;
373
+ }
374
+ if (!aHasTime && bHasTime) {
375
+ return 1;
376
+ }
363
377
  return a.id.localeCompare(b.id);
364
378
  });
365
379
 
@@ -12,7 +12,8 @@ const {
12
12
  calculateStats,
13
13
  parseDependencies,
14
14
  buildPendingItems,
15
- normalizeRunWorkItem
15
+ normalizeRunWorkItem,
16
+ normalizeTimestamp
16
17
  } = require('./model');
17
18
 
18
19
  const STANDARD_TYPES = [
@@ -80,6 +81,46 @@ function getFirstStringValue(record, keys) {
80
81
  return undefined;
81
82
  }
82
83
 
84
+ function compareByCreatedAtThenId(a, b) {
85
+ const aTime = a?.createdAt ? Date.parse(a.createdAt) : NaN;
86
+ const bTime = b?.createdAt ? Date.parse(b.createdAt) : NaN;
87
+ const aHasTime = !Number.isNaN(aTime);
88
+ const bHasTime = !Number.isNaN(bTime);
89
+
90
+ if (aHasTime && bHasTime && aTime !== bTime) {
91
+ return aTime - bTime;
92
+ }
93
+ if (aHasTime && !bHasTime) {
94
+ return -1;
95
+ }
96
+ if (!aHasTime && bHasTime) {
97
+ return 1;
98
+ }
99
+
100
+ return String(a?.id || '').localeCompare(String(b?.id || ''));
101
+ }
102
+
103
+ function compareRunDatesDesc(a, b) {
104
+ const aDate = a?.completedAt || a?.startedAt;
105
+ const bDate = b?.completedAt || b?.startedAt;
106
+ const aTime = aDate ? Date.parse(aDate) : NaN;
107
+ const bTime = bDate ? Date.parse(bDate) : NaN;
108
+ const aHasTime = !Number.isNaN(aTime);
109
+ const bHasTime = !Number.isNaN(bTime);
110
+
111
+ if (aHasTime && bHasTime && aTime !== bTime) {
112
+ return bTime - aTime;
113
+ }
114
+ if (aHasTime && !bHasTime) {
115
+ return -1;
116
+ }
117
+ if (!aHasTime && bHasTime) {
118
+ return 1;
119
+ }
120
+
121
+ return String(b?.id || '').localeCompare(String(a?.id || ''));
122
+ }
123
+
83
124
  function parseRunLog(runLogPath) {
84
125
  const content = readFileSafe(runLogPath);
85
126
  if (!content) {
@@ -210,8 +251,8 @@ function scanWorkItems(intentPath, intentId, stateWorkItems, warnings) {
210
251
  filePath,
211
252
  description: typeof frontmatter.description === 'string' ? frontmatter.description : undefined,
212
253
  dependencies,
213
- createdAt: typeof frontmatter.created === 'string' ? frontmatter.created : undefined,
214
- completedAt: typeof frontmatter.completed_at === 'string' ? frontmatter.completed_at : undefined
254
+ createdAt: normalizeTimestamp(frontmatter.created),
255
+ completedAt: normalizeTimestamp(frontmatter.completed_at)
215
256
  };
216
257
  });
217
258
  }
@@ -252,8 +293,8 @@ function scanIntents(rootPath, normalizedState, warnings) {
252
293
  filePath: briefPath,
253
294
  description: typeof frontmatter.description === 'string' ? frontmatter.description : undefined,
254
295
  workItems,
255
- createdAt: typeof frontmatter.created === 'string' ? frontmatter.created : undefined,
256
- completedAt: typeof frontmatter.completed_at === 'string' ? frontmatter.completed_at : undefined
296
+ createdAt: normalizeTimestamp(frontmatter.created),
297
+ completedAt: normalizeTimestamp(frontmatter.completed_at)
257
298
  };
258
299
  });
259
300
  }
@@ -325,7 +366,8 @@ function buildActiveRuns(runs, normalizedState) {
325
366
 
326
367
  return (normalizedState.runs?.active || [])
327
368
  .map((active) => byId.get(active.id) || null)
328
- .filter(Boolean);
369
+ .filter(Boolean)
370
+ .sort(compareRunDatesDesc);
329
371
  }
330
372
 
331
373
  function buildCompletedRuns(runs) {
@@ -430,8 +472,8 @@ function parseFireDashboard(workspacePath) {
430
472
 
431
473
  const warnings = [];
432
474
  const normalizedState = normalizeState(rawState);
433
- const intents = scanIntents(rootPath, normalizedState, warnings);
434
- const runs = scanRuns(rootPath, normalizedState);
475
+ const intents = scanIntents(rootPath, normalizedState, warnings).sort(compareByCreatedAtThenId);
476
+ const runs = scanRuns(rootPath, normalizedState).sort(compareRunDatesDesc);
435
477
  const activeRuns = buildActiveRuns(runs, normalizedState);
436
478
  const completedRuns = buildCompletedRuns(runs);
437
479
  const standards = scanStandards(rootPath);
@@ -248,7 +248,8 @@ function buildSpecsData(snapshot) {
248
248
  path: intent.path,
249
249
  storiesComplete: units.reduce((sum, unit) => sum + unit.storiesComplete, 0),
250
250
  storiesTotal: units.reduce((sum, unit) => sum + unit.storiesTotal, 0),
251
- units
251
+ units,
252
+ createdAt: intent.createdAt
252
253
  };
253
254
  });
254
255
 
@@ -400,7 +401,8 @@ function buildFireViewData(snapshot) {
400
401
  mode: normalizeFireMode(item.mode),
401
402
  complexity: normalizeFireComplexity(item.complexity),
402
403
  filePath: item.filePath,
403
- dependencies: item.dependencies || []
404
+ dependencies: item.dependencies || [],
405
+ createdAt: item.createdAt
404
406
  }));
405
407
 
406
408
  const completedRuns = (snapshot.completedRuns || []).map((run) => ({
@@ -418,13 +420,15 @@ function buildFireViewData(snapshot) {
418
420
  status: normalizeFireStatus(intent.status),
419
421
  filePath: intent.filePath,
420
422
  description: intent.description,
423
+ createdAt: intent.createdAt,
421
424
  workItems: (intent.workItems || []).map((item) => ({
422
425
  id: item.id,
423
426
  title: item.title || item.id,
424
427
  status: normalizeFireStatus(item.status),
425
428
  mode: normalizeFireMode(item.mode),
426
429
  complexity: normalizeFireComplexity(item.complexity),
427
- filePath: item.filePath
430
+ filePath: item.filePath,
431
+ createdAt: item.createdAt
428
432
  }))
429
433
  }));
430
434
 
@@ -28,6 +28,103 @@
28
28
  document.documentElement.style.colorScheme = theme;
29
29
  }
30
30
 
31
+ function copyText(text) {
32
+ if (navigator.clipboard && navigator.clipboard.writeText) {
33
+ return navigator.clipboard.writeText(text);
34
+ }
35
+
36
+ var textarea = document.createElement('textarea');
37
+ textarea.value = text;
38
+ textarea.setAttribute('readonly', 'readonly');
39
+ textarea.style.position = 'fixed';
40
+ textarea.style.opacity = '0';
41
+ document.body.appendChild(textarea);
42
+ textarea.select();
43
+
44
+ try {
45
+ document.execCommand('copy');
46
+ return Promise.resolve();
47
+ } catch (error) {
48
+ return Promise.reject(error);
49
+ } finally {
50
+ textarea.remove();
51
+ }
52
+ }
53
+
54
+ function closeCommandDialog() {
55
+ var existing = document.querySelector('.specsmd-command-dialog');
56
+ if (existing) {
57
+ existing.remove();
58
+ }
59
+ }
60
+
61
+ function showCommandDialog(command) {
62
+ closeCommandDialog();
63
+
64
+ var overlay = document.createElement('div');
65
+ overlay.className = 'specsmd-command-dialog';
66
+ overlay.setAttribute('role', 'dialog');
67
+ overlay.setAttribute('aria-modal', 'true');
68
+ overlay.setAttribute('aria-label', 'Start FIRE run command');
69
+
70
+ var panel = document.createElement('div');
71
+ panel.className = 'specsmd-command-dialog-panel';
72
+
73
+ var title = document.createElement('div');
74
+ title.className = 'specsmd-command-dialog-title';
75
+ title.textContent = 'Start FIRE run';
76
+
77
+ var description = document.createElement('div');
78
+ description.className = 'specsmd-command-dialog-description';
79
+ description.textContent = 'Run this command from your project folder.';
80
+
81
+ var commandBox = document.createElement('textarea');
82
+ commandBox.className = 'specsmd-command-dialog-command';
83
+ commandBox.value = command;
84
+ commandBox.readOnly = true;
85
+ commandBox.rows = 2;
86
+
87
+ var actions = document.createElement('div');
88
+ actions.className = 'specsmd-command-dialog-actions';
89
+
90
+ var copyButton = document.createElement('button');
91
+ copyButton.type = 'button';
92
+ copyButton.className = 'specsmd-command-dialog-copy';
93
+ copyButton.textContent = 'Copy Command';
94
+
95
+ var closeButton = document.createElement('button');
96
+ closeButton.type = 'button';
97
+ closeButton.className = 'specsmd-command-dialog-close';
98
+ closeButton.textContent = 'Close';
99
+
100
+ copyButton.addEventListener('click', function () {
101
+ copyText(command).then(function () {
102
+ copyButton.textContent = 'Copied';
103
+ }).catch(function () {
104
+ commandBox.focus();
105
+ commandBox.select();
106
+ });
107
+ });
108
+
109
+ closeButton.addEventListener('click', closeCommandDialog);
110
+ overlay.addEventListener('click', function (event) {
111
+ if (event.target === overlay) {
112
+ closeCommandDialog();
113
+ }
114
+ });
115
+
116
+ actions.appendChild(copyButton);
117
+ actions.appendChild(closeButton);
118
+ panel.appendChild(title);
119
+ panel.appendChild(description);
120
+ panel.appendChild(commandBox);
121
+ panel.appendChild(actions);
122
+ overlay.appendChild(panel);
123
+ document.body.appendChild(overlay);
124
+ commandBox.focus();
125
+ commandBox.select();
126
+ }
127
+
31
128
  document.documentElement.dataset.host = 'dashboard-web';
32
129
  applyTheme(readTheme());
33
130
 
@@ -44,4 +141,18 @@
44
141
  document.documentElement.dataset.loaded = 'true';
45
142
  }
46
143
  });
144
+
145
+ window.addEventListener('specsmd-dashboard-command', function (event) {
146
+ if (!event.detail || !event.detail.command) {
147
+ return;
148
+ }
149
+
150
+ showCommandDialog(String(event.detail.command));
151
+ });
152
+
153
+ window.addEventListener('keydown', function (event) {
154
+ if (event.key === 'Escape') {
155
+ closeCommandDialog();
156
+ }
157
+ });
47
158
  }());
@@ -64,3 +64,77 @@ specsmd-app {
64
64
  width: 100vw;
65
65
  height: 100vh;
66
66
  }
67
+
68
+ .specsmd-command-dialog {
69
+ position: fixed;
70
+ inset: 0;
71
+ z-index: 1000;
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ padding: 24px;
76
+ background: rgba(0, 0, 0, 0.45);
77
+ }
78
+
79
+ .specsmd-command-dialog-panel {
80
+ width: min(520px, 100%);
81
+ padding: 16px;
82
+ border: 1px solid var(--vscode-sideBarSectionHeader-border);
83
+ border-radius: 8px;
84
+ background: var(--vscode-sideBar-background);
85
+ color: var(--vscode-foreground);
86
+ box-shadow: 0 18px 56px rgba(0, 0, 0, 0.28);
87
+ }
88
+
89
+ .specsmd-command-dialog-title {
90
+ font-size: 15px;
91
+ font-weight: 700;
92
+ margin-bottom: 4px;
93
+ }
94
+
95
+ .specsmd-command-dialog-description {
96
+ margin-bottom: 12px;
97
+ color: var(--vscode-descriptionForeground);
98
+ font-size: 12px;
99
+ }
100
+
101
+ .specsmd-command-dialog-command {
102
+ box-sizing: border-box;
103
+ width: 100%;
104
+ min-height: 58px;
105
+ resize: vertical;
106
+ padding: 10px;
107
+ border: 1px solid var(--vscode-input-border);
108
+ border-radius: 6px;
109
+ background: var(--vscode-input-background);
110
+ color: var(--vscode-foreground);
111
+ font: 12px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
112
+ }
113
+
114
+ .specsmd-command-dialog-actions {
115
+ display: flex;
116
+ justify-content: flex-end;
117
+ gap: 8px;
118
+ margin-top: 12px;
119
+ }
120
+
121
+ .specsmd-command-dialog-actions button {
122
+ min-width: 88px;
123
+ padding: 7px 10px;
124
+ border: 1px solid var(--vscode-input-border);
125
+ border-radius: 6px;
126
+ color: var(--vscode-foreground);
127
+ background: transparent;
128
+ font: inherit;
129
+ cursor: pointer;
130
+ }
131
+
132
+ .specsmd-command-dialog-actions button:hover {
133
+ background: var(--vscode-list-hoverBackground);
134
+ }
135
+
136
+ .specsmd-command-dialog-copy {
137
+ border-color: var(--vscode-button-background) !important;
138
+ background: var(--vscode-button-background) !important;
139
+ color: var(--vscode-button-foreground) !important;
140
+ }
@@ -6644,6 +6644,15 @@
6644
6644
  connectEvents();
6645
6645
  return {
6646
6646
  postMessage(message) {
6647
+ if (isStandaloneStartRunMessage(message)) {
6648
+ window.dispatchEvent(new CustomEvent("specsmd-dashboard-command", {
6649
+ detail: {
6650
+ command: buildFireStartRunCommand(message.workItemIds),
6651
+ workItemIds: message.workItemIds
6652
+ }
6653
+ }));
6654
+ return;
6655
+ }
6647
6656
  fetch("/api/message", {
6648
6657
  method: "POST",
6649
6658
  headers: { "content-type": "application/json" },
@@ -6665,6 +6674,13 @@
6665
6674
  }
6666
6675
  };
6667
6676
  }
6677
+ function isStandaloneStartRunMessage(message) {
6678
+ return typeof message === "object" && message !== null && message.type === "startRun" && Array.isArray(message.workItemIds);
6679
+ }
6680
+ function buildFireStartRunCommand(workItemIds) {
6681
+ const ids = workItemIds.map((id) => String(id).trim()).filter(Boolean);
6682
+ return ["/specsmd-fire-builder", ...ids].join(" ");
6683
+ }
6668
6684
  var vscode = typeof acquireVsCodeApi === "function" ? acquireVsCodeApi() : createStandaloneApi();
6669
6685
 
6670
6686
  // src/webview/components/app.ts
@@ -7105,6 +7121,15 @@
7105
7121
  }
7106
7122
  }
7107
7123
  _handleFireFilterChange(e7) {
7124
+ if (this._fireData) {
7125
+ this._fireData = {
7126
+ ...this._fireData,
7127
+ intentsData: {
7128
+ ...this._fireData.intentsData,
7129
+ filter: e7.detail.filter
7130
+ }
7131
+ };
7132
+ }
7108
7133
  vscode.postMessage({ type: "fireIntentsFilter", filter: e7.detail.filter });
7109
7134
  }
7110
7135
  _handleFireToggleExpand(e7) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specsmd",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
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": {