specsmd 0.1.46 → 0.1.50
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.
- package/bin/cli.js +1 -0
- package/flows/fire/agents/builder/agent.md +2 -2
- package/flows/fire/agents/builder/skills/code-review/SKILL.md +1 -1
- package/flows/fire/agents/builder/skills/run-execute/SKILL.md +7 -7
- package/flows/fire/agents/builder/skills/run-status/SKILL.md +1 -1
- package/flows/fire/agents/orchestrator/agent.md +1 -1
- package/flows/fire/agents/orchestrator/skills/status/SKILL.md +2 -2
- package/flows/fire/memory-bank.yaml +4 -4
- package/lib/dashboard/git/worktrees.js +248 -0
- package/lib/dashboard/index.js +473 -7
- package/lib/dashboard/runtime/watch-runtime.js +18 -9
- package/lib/dashboard/tui/app.js +472 -30
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ program
|
|
|
24
24
|
.description('Live terminal dashboard for flow state (FIRE first)')
|
|
25
25
|
.option('--flow <flow>', 'Flow to inspect (fire|aidlc|simple), default auto-detect')
|
|
26
26
|
.option('--path <dir>', 'Workspace path', process.cwd())
|
|
27
|
+
.option('--worktree <nameOrPath>', 'Initial git worktree (branch name, worktree name, id, or absolute path)')
|
|
27
28
|
.option('--refresh-ms <n>', 'Fallback refresh interval in milliseconds (default: 1000)', '1000')
|
|
28
29
|
.option('--no-watch', 'Render once and exit')
|
|
29
30
|
.action((options) => dashboard.run(options));
|
|
@@ -124,7 +124,7 @@ You are the **Builder Agent** for FIRE (Fast Intent-Run Engineering).
|
|
|
124
124
|
|
|
125
125
|
```yaml
|
|
126
126
|
run:
|
|
127
|
-
id: run-001
|
|
127
|
+
id: run-fabriqa-2026-001
|
|
128
128
|
scope: batch # single | batch | wide
|
|
129
129
|
work_items:
|
|
130
130
|
- id: login-endpoint
|
|
@@ -159,7 +159,7 @@ You are the **Builder Agent** for FIRE (Fast Intent-Run Engineering).
|
|
|
159
159
|
| Create run.md | (handled by init-run.cjs) | ❌ NO direct write |
|
|
160
160
|
| Update state.yaml | (handled by scripts) | ❌ NO direct edit |
|
|
161
161
|
|
|
162
|
-
<check if="about to mkdir .specs-fire/runs/run
|
|
162
|
+
<check if="about to mkdir .specs-fire/runs/run-<worktree>-XXX">
|
|
163
163
|
<action>STOP — use init-run.cjs instead</action>
|
|
164
164
|
</check>
|
|
165
165
|
<check if="about to edit state.yaml directly">
|
|
@@ -548,8 +548,8 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
548
548
|
```json
|
|
549
549
|
{
|
|
550
550
|
"success": true,
|
|
551
|
-
"runId": "run-001",
|
|
552
|
-
"runPath": "/project/.specs-fire/runs/run-001",
|
|
551
|
+
"runId": "run-fabriqa-2026-001",
|
|
552
|
+
"runPath": "/project/.specs-fire/runs/run-fabriqa-2026-001",
|
|
553
553
|
"scope": "batch",
|
|
554
554
|
"workItems": [...],
|
|
555
555
|
"currentItem": "wi-1"
|
|
@@ -561,10 +561,10 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
561
561
|
<script name="complete-run.cjs">
|
|
562
562
|
```bash
|
|
563
563
|
# Complete current item (batch runs - moves to next item)
|
|
564
|
-
node scripts/complete-run.cjs /project run-001 --complete-item
|
|
564
|
+
node scripts/complete-run.cjs /project run-fabriqa-2026-001 --complete-item
|
|
565
565
|
|
|
566
566
|
# Complete entire run (single runs or final item in batch)
|
|
567
|
-
node scripts/complete-run.cjs /project run-001 --complete-run \
|
|
567
|
+
node scripts/complete-run.cjs /project run-fabriqa-2026-001 --complete-run \
|
|
568
568
|
--files-created='[{"path":"src/new.ts","purpose":"New feature"}]' \
|
|
569
569
|
--files-modified='[{"path":"src/old.ts","changes":"Added import"}]' \
|
|
570
570
|
--tests=5 --coverage=85
|
|
@@ -574,7 +574,7 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
574
574
|
```json
|
|
575
575
|
{
|
|
576
576
|
"success": true,
|
|
577
|
-
"runId": "run-001",
|
|
577
|
+
"runId": "run-fabriqa-2026-001",
|
|
578
578
|
"completedItem": "wi-1",
|
|
579
579
|
"nextItem": "wi-2",
|
|
580
580
|
"remainingItems": 1,
|
|
@@ -587,7 +587,7 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
587
587
|
```json
|
|
588
588
|
{
|
|
589
589
|
"success": true,
|
|
590
|
-
"runId": "run-001",
|
|
590
|
+
"runId": "run-fabriqa-2026-001",
|
|
591
591
|
"scope": "batch",
|
|
592
592
|
"workItemsCompleted": 2,
|
|
593
593
|
"completedAt": "2026-01-20T..."
|
|
@@ -619,7 +619,7 @@ Supports both single-item and multi-item (batch/wide) runs.
|
|
|
619
619
|
After init-run.cjs creates a run:
|
|
620
620
|
|
|
621
621
|
```
|
|
622
|
-
.specs-fire/runs/run-001/
|
|
622
|
+
.specs-fire/runs/run-fabriqa-2026-001/
|
|
623
623
|
├── run.md # Created by init-run.cjs, updated by complete-run.cjs
|
|
624
624
|
├── plan.md # Created BEFORE implementation (ALL modes - required)
|
|
625
625
|
├── test-report.md # Created AFTER tests pass (required)
|
|
@@ -103,7 +103,7 @@ You are the **Orchestrator Agent** for FIRE (Fast Intent-Run Engineering).
|
|
|
103
103
|
runs:
|
|
104
104
|
active: [] # List of active runs (supports multiple parallel runs)
|
|
105
105
|
completed:
|
|
106
|
-
- id: run-001
|
|
106
|
+
- id: run-fabriqa-2026-001
|
|
107
107
|
work_items:
|
|
108
108
|
- id: login-endpoint
|
|
109
109
|
intent: user-auth
|
|
@@ -639,7 +639,7 @@ Detect inconsistencies and offer interactive resolution.
|
|
|
639
639
|
|
|
640
640
|
## Active Runs
|
|
641
641
|
|
|
642
|
-
- **Run**: run-002 | **Scope**: single
|
|
642
|
+
- **Run**: run-fabriqa-2026-002 | **Scope**: single
|
|
643
643
|
- **Current Item**: session-management
|
|
644
644
|
- **Started**: 2026-01-19T10:30:00Z
|
|
645
645
|
|
|
@@ -657,7 +657,7 @@ Detect inconsistencies and offer interactive resolution.
|
|
|
657
657
|
|
|
658
658
|
| # | Type | Location | Issue | Suggested Fix |
|
|
659
659
|
|---|------|----------|-------|---------------|
|
|
660
|
-
| 1 | 🟡 | run-002 | Run started 3 days ago, may be stale | Resume or abandon |
|
|
660
|
+
| 1 | 🟡 | run-fabriqa-2026-002 | Run started 3 days ago, may be stale | Resume or abandon |
|
|
661
661
|
| 2 | 🔵 | login-endpoint | Frontmatter says 'pending' but state says 'completed' | Sync frontmatter |
|
|
662
662
|
| 3 | 🟡 | analytics-dashboard.md | Work item on disk but not tracked | Add to state.yaml |
|
|
663
663
|
|
|
@@ -66,7 +66,7 @@ state:
|
|
|
66
66
|
- active: "List of currently active runs (supports multiple parallel runs)"
|
|
67
67
|
- completed: "List of completed runs with id, work_item, intent, completed timestamp"
|
|
68
68
|
# Each run (active or completed) has:
|
|
69
|
-
# - id: "Run ID (e.g., run-001)"
|
|
69
|
+
# - id: "Run ID (e.g., run-fabriqa-2026-001)"
|
|
70
70
|
# - scope: "single | batch | wide"
|
|
71
71
|
# - work_items: "List of work items in this run"
|
|
72
72
|
# - current_item: "Work item currently being executed (active runs only)"
|
|
@@ -93,9 +93,9 @@ naming:
|
|
|
93
93
|
note: "Kebab-case, action-oriented name"
|
|
94
94
|
|
|
95
95
|
runs:
|
|
96
|
-
format: "run-{NNN}/"
|
|
97
|
-
example: "run-001/"
|
|
98
|
-
note: "Sequential 3-digit number, folder per run"
|
|
96
|
+
format: "run-{worktree}-{NNN}/"
|
|
97
|
+
example: "run-fabriqa-2026-001/"
|
|
98
|
+
note: "Sequential 3-digit number per worktree, folder per run"
|
|
99
99
|
contents:
|
|
100
100
|
- "run.md" # Run log (created by init-run.cjs)
|
|
101
101
|
- "plan.md" # Implementation plan (ALL modes, created BEFORE implementation)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
function normalizePath(value) {
|
|
6
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
return path.resolve(value.trim());
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseBranchName(refLine) {
|
|
17
|
+
if (typeof refLine !== 'string' || refLine.trim() === '') {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
const prefix = 'refs/heads/';
|
|
21
|
+
if (refLine.startsWith(prefix)) {
|
|
22
|
+
return refLine.slice(prefix.length);
|
|
23
|
+
}
|
|
24
|
+
return refLine.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildWorktreeId(worktreePath) {
|
|
28
|
+
return String(worktreePath || '')
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/[^a-z0-9/_-]+/g, '-');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseGitWorktreePorcelain(rawOutput, fallbackWorkspacePath = process.cwd()) {
|
|
34
|
+
const text = String(rawOutput || '');
|
|
35
|
+
const blocks = text
|
|
36
|
+
.split(/\n\s*\n/g)
|
|
37
|
+
.map((block) => block.trim())
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
|
|
40
|
+
const worktrees = [];
|
|
41
|
+
for (const block of blocks) {
|
|
42
|
+
const lines = block.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
43
|
+
if (lines.length === 0) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const pathLine = lines.find((line) => line.startsWith('worktree '));
|
|
48
|
+
if (!pathLine) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const worktreePath = normalizePath(pathLine.slice('worktree '.length));
|
|
53
|
+
if (!worktreePath) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const branchRef = (lines.find((line) => line.startsWith('branch ')) || '').slice('branch '.length);
|
|
58
|
+
const head = (lines.find((line) => line.startsWith('HEAD ')) || '').slice('HEAD '.length);
|
|
59
|
+
const detached = lines.includes('detached');
|
|
60
|
+
const prunable = lines.some((line) => line.startsWith('prunable'));
|
|
61
|
+
const locked = lines.some((line) => line.startsWith('locked'));
|
|
62
|
+
const branch = parseBranchName(branchRef);
|
|
63
|
+
const name = path.basename(worktreePath);
|
|
64
|
+
const displayBranch = detached ? `[detached:${head.slice(0, 7) || 'unknown'}]` : (branch || '[unknown]');
|
|
65
|
+
|
|
66
|
+
worktrees.push({
|
|
67
|
+
id: buildWorktreeId(worktreePath),
|
|
68
|
+
path: worktreePath,
|
|
69
|
+
name,
|
|
70
|
+
branch,
|
|
71
|
+
displayBranch,
|
|
72
|
+
head: head || '',
|
|
73
|
+
detached,
|
|
74
|
+
prunable,
|
|
75
|
+
locked,
|
|
76
|
+
isMainBranch: branch === 'main' || branch === 'master',
|
|
77
|
+
isCurrentPath: false
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (worktrees.length === 0) {
|
|
82
|
+
const fallbackPath = normalizePath(fallbackWorkspacePath) || normalizePath(process.cwd()) || process.cwd();
|
|
83
|
+
return [{
|
|
84
|
+
id: buildWorktreeId(fallbackPath),
|
|
85
|
+
path: fallbackPath,
|
|
86
|
+
name: path.basename(fallbackPath),
|
|
87
|
+
branch: '',
|
|
88
|
+
displayBranch: '[non-git]',
|
|
89
|
+
head: '',
|
|
90
|
+
detached: false,
|
|
91
|
+
prunable: false,
|
|
92
|
+
locked: false,
|
|
93
|
+
isMainBranch: false,
|
|
94
|
+
isCurrentPath: true
|
|
95
|
+
}];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return worktrees;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function markCurrentWorktree(worktrees, workspacePath) {
|
|
102
|
+
const currentPath = normalizePath(workspacePath);
|
|
103
|
+
const safeWorktrees = Array.isArray(worktrees) ? worktrees : [];
|
|
104
|
+
const marked = safeWorktrees.map((worktree) => ({
|
|
105
|
+
...worktree,
|
|
106
|
+
isCurrentPath: currentPath != null && normalizePath(worktree.path) === currentPath
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
if (marked.some((worktree) => worktree.isCurrentPath)) {
|
|
110
|
+
return marked;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (currentPath) {
|
|
114
|
+
return marked.map((worktree, index) => ({
|
|
115
|
+
...worktree,
|
|
116
|
+
isCurrentPath: index === 0
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return marked;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sortWorktrees(worktrees) {
|
|
124
|
+
const safeWorktrees = Array.isArray(worktrees) ? [...worktrees] : [];
|
|
125
|
+
return safeWorktrees.sort((a, b) => {
|
|
126
|
+
if (a.isCurrentPath !== b.isCurrentPath) {
|
|
127
|
+
return a.isCurrentPath ? -1 : 1;
|
|
128
|
+
}
|
|
129
|
+
if (a.isMainBranch !== b.isMainBranch) {
|
|
130
|
+
return a.isMainBranch ? -1 : 1;
|
|
131
|
+
}
|
|
132
|
+
return String(a.displayBranch || a.name || '').localeCompare(String(b.displayBranch || b.name || ''));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isGitWorkspace(workspacePath) {
|
|
137
|
+
const cwd = normalizePath(workspacePath) || process.cwd();
|
|
138
|
+
const result = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
139
|
+
cwd,
|
|
140
|
+
encoding: 'utf8'
|
|
141
|
+
});
|
|
142
|
+
if (result.error || result.status !== 0) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return String(result.stdout || '').trim() === 'true';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function discoverGitWorktrees(workspacePath) {
|
|
149
|
+
const cwd = normalizePath(workspacePath) || process.cwd();
|
|
150
|
+
if (!isGitWorkspace(cwd)) {
|
|
151
|
+
return {
|
|
152
|
+
worktrees: markCurrentWorktree(parseGitWorktreePorcelain('', cwd), cwd),
|
|
153
|
+
source: 'fallback',
|
|
154
|
+
isGitRepo: false
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const result = spawnSync('git', ['worktree', 'list', '--porcelain'], {
|
|
159
|
+
cwd,
|
|
160
|
+
encoding: 'utf8'
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (result.error || result.status !== 0) {
|
|
164
|
+
return {
|
|
165
|
+
worktrees: markCurrentWorktree(parseGitWorktreePorcelain('', cwd), cwd),
|
|
166
|
+
source: 'fallback',
|
|
167
|
+
isGitRepo: true,
|
|
168
|
+
error: result.error ? result.error.message : String(result.stderr || '').trim()
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const parsed = parseGitWorktreePorcelain(result.stdout, cwd);
|
|
173
|
+
const marked = markCurrentWorktree(parsed, cwd);
|
|
174
|
+
return {
|
|
175
|
+
worktrees: sortWorktrees(marked),
|
|
176
|
+
source: 'git',
|
|
177
|
+
isGitRepo: true
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function pickWorktree(worktrees, selector, workspacePath) {
|
|
182
|
+
const safeWorktrees = Array.isArray(worktrees) ? worktrees : [];
|
|
183
|
+
if (safeWorktrees.length === 0) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const normalizedSelector = String(selector || '').trim();
|
|
188
|
+
const selectorPath = normalizePath(normalizedSelector);
|
|
189
|
+
const currentPath = normalizePath(workspacePath);
|
|
190
|
+
|
|
191
|
+
if (normalizedSelector !== '') {
|
|
192
|
+
const byId = safeWorktrees.find((item) => item.id === normalizedSelector);
|
|
193
|
+
if (byId) {
|
|
194
|
+
return byId;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (selectorPath) {
|
|
198
|
+
const byPath = safeWorktrees.find((item) => normalizePath(item.path) === selectorPath);
|
|
199
|
+
if (byPath) {
|
|
200
|
+
return byPath;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const byBranch = safeWorktrees.find((item) => item.branch === normalizedSelector || item.displayBranch === normalizedSelector);
|
|
205
|
+
if (byBranch) {
|
|
206
|
+
return byBranch;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const byName = safeWorktrees.find((item) => item.name === normalizedSelector);
|
|
210
|
+
if (byName) {
|
|
211
|
+
return byName;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (currentPath) {
|
|
216
|
+
const byCurrentPath = safeWorktrees.find((item) => normalizePath(item.path) === currentPath);
|
|
217
|
+
if (byCurrentPath) {
|
|
218
|
+
return byCurrentPath;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const markedCurrent = safeWorktrees.find((item) => item.isCurrentPath);
|
|
223
|
+
if (markedCurrent) {
|
|
224
|
+
return markedCurrent;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return safeWorktrees[0];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function pathExistsAsDirectory(targetPath) {
|
|
231
|
+
try {
|
|
232
|
+
return fs.statSync(targetPath).isDirectory();
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = {
|
|
239
|
+
normalizePath,
|
|
240
|
+
parseBranchName,
|
|
241
|
+
parseGitWorktreePorcelain,
|
|
242
|
+
markCurrentWorktree,
|
|
243
|
+
sortWorktrees,
|
|
244
|
+
isGitWorkspace,
|
|
245
|
+
discoverGitWorktrees,
|
|
246
|
+
pickWorktree,
|
|
247
|
+
pathExistsAsDirectory
|
|
248
|
+
};
|