agileflow 2.44.0 → 2.45.0
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/package.json +1 -1
- package/scripts/README.md +267 -0
- package/scripts/agileflow-configure.js +927 -0
- package/scripts/agileflow-statusline.sh +355 -0
- package/scripts/agileflow-stop.sh +13 -0
- package/scripts/agileflow-welcome.js +427 -0
- package/scripts/archive-completed-stories.sh +162 -0
- package/scripts/clear-active-command.js +48 -0
- package/scripts/compress-status.sh +116 -0
- package/scripts/expertise-metrics.sh +264 -0
- package/scripts/get-env.js +209 -0
- package/scripts/obtain-context.js +293 -0
- package/scripts/precompact-context.sh +123 -0
- package/scripts/validate-expertise.sh +259 -0
- package/tools/cli/installers/core/installer.js +97 -4
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* agileflow-welcome.js - Beautiful SessionStart welcome display
|
|
5
|
+
*
|
|
6
|
+
* Shows a transparent ASCII table with:
|
|
7
|
+
* - Project info (name, version, branch, commit)
|
|
8
|
+
* - Story stats (WIP, blocked, completed)
|
|
9
|
+
* - Archival status
|
|
10
|
+
* - Session cleanup status
|
|
11
|
+
* - Last commit
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
// ANSI color codes
|
|
19
|
+
const c = {
|
|
20
|
+
reset: '\x1b[0m',
|
|
21
|
+
bold: '\x1b[1m',
|
|
22
|
+
dim: '\x1b[2m',
|
|
23
|
+
|
|
24
|
+
red: '\x1b[31m',
|
|
25
|
+
green: '\x1b[32m',
|
|
26
|
+
yellow: '\x1b[33m',
|
|
27
|
+
blue: '\x1b[34m',
|
|
28
|
+
magenta: '\x1b[35m',
|
|
29
|
+
cyan: '\x1b[36m',
|
|
30
|
+
|
|
31
|
+
brightBlack: '\x1b[90m',
|
|
32
|
+
brightGreen: '\x1b[92m',
|
|
33
|
+
brightYellow: '\x1b[93m',
|
|
34
|
+
brightCyan: '\x1b[96m',
|
|
35
|
+
|
|
36
|
+
// Brand color (#e8683a)
|
|
37
|
+
brand: '\x1b[38;2;232;104;58m',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Box drawing characters
|
|
41
|
+
const box = {
|
|
42
|
+
tl: '╭', tr: '╮', bl: '╰', br: '╯',
|
|
43
|
+
h: '─', v: '│',
|
|
44
|
+
lT: '├', rT: '┤', tT: '┬', bT: '┴',
|
|
45
|
+
cross: '┼',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function getProjectRoot() {
|
|
49
|
+
let dir = process.cwd();
|
|
50
|
+
while (!fs.existsSync(path.join(dir, '.agileflow')) && dir !== '/') {
|
|
51
|
+
dir = path.dirname(dir);
|
|
52
|
+
}
|
|
53
|
+
return dir !== '/' ? dir : process.cwd();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getProjectInfo(rootDir) {
|
|
57
|
+
const info = {
|
|
58
|
+
name: 'agileflow',
|
|
59
|
+
version: 'unknown',
|
|
60
|
+
branch: 'unknown',
|
|
61
|
+
commit: 'unknown',
|
|
62
|
+
lastCommit: '',
|
|
63
|
+
wipCount: 0,
|
|
64
|
+
blockedCount: 0,
|
|
65
|
+
completedCount: 0,
|
|
66
|
+
readyCount: 0,
|
|
67
|
+
totalStories: 0,
|
|
68
|
+
currentStory: null,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Get package info
|
|
72
|
+
try {
|
|
73
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, 'packages/cli/package.json'), 'utf8'));
|
|
74
|
+
info.version = pkg.version || info.version;
|
|
75
|
+
} catch (e) {
|
|
76
|
+
try {
|
|
77
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'));
|
|
78
|
+
info.version = pkg.version || info.version;
|
|
79
|
+
} catch (e2) {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Get git info
|
|
83
|
+
try {
|
|
84
|
+
info.branch = execSync('git branch --show-current', { cwd: rootDir, encoding: 'utf8' }).trim();
|
|
85
|
+
info.commit = execSync('git rev-parse --short HEAD', { cwd: rootDir, encoding: 'utf8' }).trim();
|
|
86
|
+
info.lastCommit = execSync('git log -1 --format="%s"', { cwd: rootDir, encoding: 'utf8' }).trim();
|
|
87
|
+
} catch (e) {}
|
|
88
|
+
|
|
89
|
+
// Get status info
|
|
90
|
+
try {
|
|
91
|
+
const statusPath = path.join(rootDir, 'docs/09-agents/status.json');
|
|
92
|
+
if (fs.existsSync(statusPath)) {
|
|
93
|
+
const status = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
|
94
|
+
if (status.stories) {
|
|
95
|
+
for (const [id, story] of Object.entries(status.stories)) {
|
|
96
|
+
info.totalStories++;
|
|
97
|
+
if (story.status === 'in_progress') {
|
|
98
|
+
info.wipCount++;
|
|
99
|
+
if (!info.currentStory) {
|
|
100
|
+
info.currentStory = { id, title: story.title };
|
|
101
|
+
}
|
|
102
|
+
} else if (story.status === 'blocked') {
|
|
103
|
+
info.blockedCount++;
|
|
104
|
+
} else if (story.status === 'completed') {
|
|
105
|
+
info.completedCount++;
|
|
106
|
+
} else if (story.status === 'ready') {
|
|
107
|
+
info.readyCount++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch (e) {}
|
|
113
|
+
|
|
114
|
+
return info;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function runArchival(rootDir) {
|
|
118
|
+
const result = { ran: false, threshold: 7, archived: 0, remaining: 0 };
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
|
|
122
|
+
if (fs.existsSync(metadataPath)) {
|
|
123
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
124
|
+
if (metadata.archival?.enabled === false) {
|
|
125
|
+
result.disabled = true;
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
result.threshold = metadata.archival?.threshold_days || 7;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const statusPath = path.join(rootDir, 'docs/09-agents/status.json');
|
|
132
|
+
if (!fs.existsSync(statusPath)) return result;
|
|
133
|
+
|
|
134
|
+
const status = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
|
135
|
+
const stories = status.stories || {};
|
|
136
|
+
|
|
137
|
+
const cutoffDate = new Date();
|
|
138
|
+
cutoffDate.setDate(cutoffDate.getDate() - result.threshold);
|
|
139
|
+
|
|
140
|
+
let toArchiveCount = 0;
|
|
141
|
+
for (const [id, story] of Object.entries(stories)) {
|
|
142
|
+
if (story.status === 'completed' && story.completed_at) {
|
|
143
|
+
if (new Date(story.completed_at) < cutoffDate) {
|
|
144
|
+
toArchiveCount++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
result.ran = true;
|
|
150
|
+
result.remaining = Object.keys(stories).length;
|
|
151
|
+
|
|
152
|
+
if (toArchiveCount > 0) {
|
|
153
|
+
// Run archival
|
|
154
|
+
try {
|
|
155
|
+
execSync('bash scripts/archive-completed-stories.sh', {
|
|
156
|
+
cwd: rootDir,
|
|
157
|
+
encoding: 'utf8',
|
|
158
|
+
stdio: 'pipe'
|
|
159
|
+
});
|
|
160
|
+
result.archived = toArchiveCount;
|
|
161
|
+
result.remaining -= toArchiveCount;
|
|
162
|
+
} catch (e) {}
|
|
163
|
+
}
|
|
164
|
+
} catch (e) {}
|
|
165
|
+
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function clearActiveCommands(rootDir) {
|
|
170
|
+
const result = { ran: false, cleared: 0, commandNames: [] };
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const sessionStatePath = path.join(rootDir, 'docs/09-agents/session-state.json');
|
|
174
|
+
if (!fs.existsSync(sessionStatePath)) return result;
|
|
175
|
+
|
|
176
|
+
const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
177
|
+
result.ran = true;
|
|
178
|
+
|
|
179
|
+
if (state.active_commands && state.active_commands.length > 0) {
|
|
180
|
+
result.cleared = state.active_commands.length;
|
|
181
|
+
// Capture command names before clearing
|
|
182
|
+
for (const cmd of state.active_commands) {
|
|
183
|
+
if (cmd.name) result.commandNames.push(cmd.name);
|
|
184
|
+
}
|
|
185
|
+
state.active_commands = [];
|
|
186
|
+
}
|
|
187
|
+
if (state.active_command !== undefined) {
|
|
188
|
+
result.cleared++;
|
|
189
|
+
// Capture single command name
|
|
190
|
+
if (state.active_command.name) {
|
|
191
|
+
result.commandNames.push(state.active_command.name);
|
|
192
|
+
}
|
|
193
|
+
delete state.active_command;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (result.cleared > 0) {
|
|
197
|
+
fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
|
|
198
|
+
}
|
|
199
|
+
} catch (e) {}
|
|
200
|
+
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function checkPreCompact(rootDir) {
|
|
205
|
+
const result = { configured: false, scriptExists: false, version: null, outdated: false };
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
// Check if PreCompact hook is configured in settings
|
|
209
|
+
const settingsPath = path.join(rootDir, '.claude/settings.json');
|
|
210
|
+
if (fs.existsSync(settingsPath)) {
|
|
211
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
212
|
+
if (settings.hooks?.PreCompact?.length > 0) {
|
|
213
|
+
result.configured = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check if the script exists
|
|
218
|
+
const scriptPath = path.join(rootDir, 'scripts/precompact-context.sh');
|
|
219
|
+
if (fs.existsSync(scriptPath)) {
|
|
220
|
+
result.scriptExists = true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check configured version from metadata
|
|
224
|
+
const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
|
|
225
|
+
if (fs.existsSync(metadataPath)) {
|
|
226
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
227
|
+
if (metadata.features?.precompact?.configured_version) {
|
|
228
|
+
result.version = metadata.features.precompact.configured_version;
|
|
229
|
+
// PreCompact v2.40.0+ has multi-command support
|
|
230
|
+
result.outdated = compareVersions(result.version, '2.40.0') < 0;
|
|
231
|
+
} else if (result.configured) {
|
|
232
|
+
// Hook exists but no version tracked = definitely outdated
|
|
233
|
+
result.outdated = true;
|
|
234
|
+
result.version = 'unknown';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {}
|
|
238
|
+
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Compare semantic versions: returns -1 if a < b, 0 if equal, 1 if a > b
|
|
243
|
+
function compareVersions(a, b) {
|
|
244
|
+
if (!a || !b) return 0;
|
|
245
|
+
const partsA = a.split('.').map(Number);
|
|
246
|
+
const partsB = b.split('.').map(Number);
|
|
247
|
+
for (let i = 0; i < 3; i++) {
|
|
248
|
+
const numA = partsA[i] || 0;
|
|
249
|
+
const numB = partsB[i] || 0;
|
|
250
|
+
if (numA < numB) return -1;
|
|
251
|
+
if (numA > numB) return 1;
|
|
252
|
+
}
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function getFeatureVersions(rootDir) {
|
|
257
|
+
const result = {
|
|
258
|
+
hooks: { version: null, outdated: false },
|
|
259
|
+
archival: { version: null, outdated: false },
|
|
260
|
+
statusline: { version: null, outdated: false },
|
|
261
|
+
precompact: { version: null, outdated: false }
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Minimum compatible versions for each feature
|
|
265
|
+
const minVersions = {
|
|
266
|
+
hooks: '2.35.0',
|
|
267
|
+
archival: '2.35.0',
|
|
268
|
+
statusline: '2.35.0',
|
|
269
|
+
precompact: '2.40.0' // Multi-command support
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
|
|
274
|
+
if (fs.existsSync(metadataPath)) {
|
|
275
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
276
|
+
|
|
277
|
+
for (const feature of Object.keys(result)) {
|
|
278
|
+
if (metadata.features?.[feature]?.configured_version) {
|
|
279
|
+
result[feature].version = metadata.features[feature].configured_version;
|
|
280
|
+
result[feature].outdated = compareVersions(result[feature].version, minVersions[feature]) < 0;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch (e) {}
|
|
285
|
+
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function pad(str, len, align = 'left') {
|
|
290
|
+
const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
291
|
+
const diff = len - stripped.length;
|
|
292
|
+
if (diff <= 0) return str;
|
|
293
|
+
if (align === 'right') return ' '.repeat(diff) + str;
|
|
294
|
+
if (align === 'center') return ' '.repeat(Math.floor(diff/2)) + str + ' '.repeat(Math.ceil(diff/2));
|
|
295
|
+
return str + ' '.repeat(diff);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Truncate string to max length, respecting ANSI codes
|
|
299
|
+
function truncate(str, maxLen, suffix = '..') {
|
|
300
|
+
const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
301
|
+
if (stripped.length <= maxLen) return str;
|
|
302
|
+
|
|
303
|
+
// Find position in original string that corresponds to maxLen - suffix.length visible chars
|
|
304
|
+
const targetLen = maxLen - suffix.length;
|
|
305
|
+
let visibleCount = 0;
|
|
306
|
+
let cutIndex = 0;
|
|
307
|
+
let inEscape = false;
|
|
308
|
+
|
|
309
|
+
for (let i = 0; i < str.length; i++) {
|
|
310
|
+
if (str[i] === '\x1b') {
|
|
311
|
+
inEscape = true;
|
|
312
|
+
} else if (inEscape && str[i] === 'm') {
|
|
313
|
+
inEscape = false;
|
|
314
|
+
} else if (!inEscape) {
|
|
315
|
+
visibleCount++;
|
|
316
|
+
if (visibleCount >= targetLen) {
|
|
317
|
+
cutIndex = i + 1;
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return str.substring(0, cutIndex) + suffix;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function formatTable(info, archival, session, precompact) {
|
|
327
|
+
const W = 58; // inner width
|
|
328
|
+
const R = W - 24; // right column width (34 chars)
|
|
329
|
+
const lines = [];
|
|
330
|
+
|
|
331
|
+
// Helper to create a row (auto-truncates right content to fit)
|
|
332
|
+
const row = (left, right, leftColor = '', rightColor = '') => {
|
|
333
|
+
const leftStr = `${leftColor}${left}${leftColor ? c.reset : ''}`;
|
|
334
|
+
const rightTrunc = truncate(right, R);
|
|
335
|
+
const rightStr = `${rightColor}${rightTrunc}${rightColor ? c.reset : ''}`;
|
|
336
|
+
return `${c.dim}${box.v}${c.reset} ${pad(leftStr, 20)} ${c.dim}${box.v}${c.reset} ${pad(rightStr, R)} ${c.dim}${box.v}${c.reset}`;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const divider = () => `${c.dim}${box.lT}${box.h.repeat(22)}${box.cross}${box.h.repeat(W - 22)}${box.rT}${c.reset}`;
|
|
340
|
+
const topBorder = `${c.dim}${box.tl}${box.h.repeat(22)}${box.tT}${box.h.repeat(W - 22)}${box.tr}${c.reset}`;
|
|
341
|
+
const bottomBorder = `${c.dim}${box.bl}${box.h.repeat(22)}${box.bT}${box.h.repeat(W - 22)}${box.br}${c.reset}`;
|
|
342
|
+
|
|
343
|
+
// Header (truncate branch name if too long)
|
|
344
|
+
const branchColor = info.branch === 'main' ? c.green : info.branch.startsWith('fix') ? c.red : c.cyan;
|
|
345
|
+
// Fixed parts: "agileflow " (10) + "v" (1) + version + " " (2) + " (" (2) + commit (7) + ")" (1) = 23 + version.length
|
|
346
|
+
const maxBranchLen = (W - 1) - 23 - info.version.length;
|
|
347
|
+
const branchDisplay = info.branch.length > maxBranchLen
|
|
348
|
+
? info.branch.substring(0, maxBranchLen - 2) + '..'
|
|
349
|
+
: info.branch;
|
|
350
|
+
const header = `${c.brand}${c.bold}agileflow${c.reset} ${c.dim}v${info.version}${c.reset} ${branchColor}${branchDisplay}${c.reset} ${c.dim}(${info.commit})${c.reset}`;
|
|
351
|
+
const headerLine = `${c.dim}${box.v}${c.reset} ${pad(header, W - 1)} ${c.dim}${box.v}${c.reset}`;
|
|
352
|
+
|
|
353
|
+
lines.push(topBorder);
|
|
354
|
+
lines.push(headerLine);
|
|
355
|
+
lines.push(divider());
|
|
356
|
+
|
|
357
|
+
// Stories section
|
|
358
|
+
lines.push(row('In Progress', info.wipCount > 0 ? `${info.wipCount}` : '0', c.dim, info.wipCount > 0 ? c.yellow : c.dim));
|
|
359
|
+
lines.push(row('Blocked', info.blockedCount > 0 ? `${info.blockedCount}` : '0', c.dim, info.blockedCount > 0 ? c.red : c.dim));
|
|
360
|
+
lines.push(row('Ready', info.readyCount > 0 ? `${info.readyCount}` : '0', c.dim, info.readyCount > 0 ? c.cyan : c.dim));
|
|
361
|
+
lines.push(row('Completed', info.completedCount > 0 ? `${info.completedCount}` : '0', c.dim, info.completedCount > 0 ? c.green : c.dim));
|
|
362
|
+
|
|
363
|
+
lines.push(divider());
|
|
364
|
+
|
|
365
|
+
// Archival section
|
|
366
|
+
if (archival.disabled) {
|
|
367
|
+
lines.push(row('Auto-archival', 'disabled', c.dim, c.dim));
|
|
368
|
+
} else {
|
|
369
|
+
const archivalStatus = archival.archived > 0
|
|
370
|
+
? `archived ${archival.archived} stories`
|
|
371
|
+
: `nothing to archive`;
|
|
372
|
+
lines.push(row('Auto-archival', archivalStatus, c.dim, archival.archived > 0 ? c.green : c.dim));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Session cleanup
|
|
376
|
+
const sessionStatus = session.cleared > 0
|
|
377
|
+
? `cleared ${session.cleared} command(s)`
|
|
378
|
+
: `clean`;
|
|
379
|
+
lines.push(row('Session state', sessionStatus, c.dim, session.cleared > 0 ? c.green : c.dim));
|
|
380
|
+
|
|
381
|
+
// PreCompact status with version check
|
|
382
|
+
if (precompact.configured && precompact.scriptExists) {
|
|
383
|
+
if (precompact.outdated) {
|
|
384
|
+
const verStr = precompact.version ? ` (v${precompact.version})` : '';
|
|
385
|
+
lines.push(row('Context preserve', `outdated${verStr}`, c.dim, c.yellow));
|
|
386
|
+
} else if (session.commandNames && session.commandNames.length > 0) {
|
|
387
|
+
// Show the preserved command names
|
|
388
|
+
const cmdDisplay = session.commandNames.map(n => `/agileflow:${n}`).join(', ');
|
|
389
|
+
lines.push(row('Context preserve', cmdDisplay, c.dim, c.green));
|
|
390
|
+
} else {
|
|
391
|
+
lines.push(row('Context preserve', 'nothing to compact', c.dim, c.dim));
|
|
392
|
+
}
|
|
393
|
+
} else if (precompact.configured) {
|
|
394
|
+
lines.push(row('Context preserve', 'script missing', c.dim, c.yellow));
|
|
395
|
+
} else {
|
|
396
|
+
lines.push(row('Context preserve', 'not configured', c.dim, c.dim));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
lines.push(divider());
|
|
400
|
+
|
|
401
|
+
// Current story (if any) - row() auto-truncates
|
|
402
|
+
if (info.currentStory) {
|
|
403
|
+
lines.push(row('Current', `${c.blue}${info.currentStory.id}${c.reset}: ${info.currentStory.title}`, c.dim, ''));
|
|
404
|
+
} else {
|
|
405
|
+
lines.push(row('Current', 'No active story', c.dim, c.dim));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Last commit - row() auto-truncates
|
|
409
|
+
lines.push(row('Last commit', `${info.commit} ${info.lastCommit}`, c.dim, c.dim));
|
|
410
|
+
|
|
411
|
+
lines.push(bottomBorder);
|
|
412
|
+
|
|
413
|
+
return lines.join('\n');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Main
|
|
417
|
+
function main() {
|
|
418
|
+
const rootDir = getProjectRoot();
|
|
419
|
+
const info = getProjectInfo(rootDir);
|
|
420
|
+
const archival = runArchival(rootDir);
|
|
421
|
+
const session = clearActiveCommands(rootDir);
|
|
422
|
+
const precompact = checkPreCompact(rootDir);
|
|
423
|
+
|
|
424
|
+
console.log(formatTable(info, archival, session, precompact));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
main();
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# archive-completed-stories.sh
|
|
4
|
+
# Automatically archives completed stories older than threshold from status.json
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
# Colors for output
|
|
9
|
+
RED='\033[0;31m'
|
|
10
|
+
GREEN='\033[0;32m'
|
|
11
|
+
YELLOW='\033[1;33m'
|
|
12
|
+
BLUE='\033[0;34m'
|
|
13
|
+
NC='\033[0m' # No Color
|
|
14
|
+
|
|
15
|
+
# Default paths (relative to project root)
|
|
16
|
+
DOCS_DIR="docs"
|
|
17
|
+
STATUS_FILE="$DOCS_DIR/09-agents/status.json"
|
|
18
|
+
ARCHIVE_DIR="$DOCS_DIR/09-agents/archive"
|
|
19
|
+
METADATA_FILE="$DOCS_DIR/00-meta/agileflow-metadata.json"
|
|
20
|
+
|
|
21
|
+
# Find project root (directory containing .agileflow)
|
|
22
|
+
PROJECT_ROOT="$(pwd)"
|
|
23
|
+
while [[ ! -d "$PROJECT_ROOT/.agileflow" ]] && [[ "$PROJECT_ROOT" != "/" ]]; do
|
|
24
|
+
PROJECT_ROOT="$(dirname "$PROJECT_ROOT")"
|
|
25
|
+
done
|
|
26
|
+
|
|
27
|
+
if [[ "$PROJECT_ROOT" == "/" ]]; then
|
|
28
|
+
echo -e "${RED}Error: Not in an AgileFlow project (no .agileflow directory found)${NC}"
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Update paths to absolute
|
|
33
|
+
STATUS_FILE="$PROJECT_ROOT/$STATUS_FILE"
|
|
34
|
+
ARCHIVE_DIR="$PROJECT_ROOT/$ARCHIVE_DIR"
|
|
35
|
+
METADATA_FILE="$PROJECT_ROOT/$METADATA_FILE"
|
|
36
|
+
|
|
37
|
+
# Check if status.json exists
|
|
38
|
+
if [[ ! -f "$STATUS_FILE" ]]; then
|
|
39
|
+
echo -e "${YELLOW}No status.json found at $STATUS_FILE${NC}"
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Read archival settings
|
|
44
|
+
THRESHOLD_DAYS=7
|
|
45
|
+
ENABLED=true
|
|
46
|
+
|
|
47
|
+
if [[ -f "$METADATA_FILE" ]]; then
|
|
48
|
+
if command -v jq &> /dev/null; then
|
|
49
|
+
ENABLED=$(jq -r '.archival.enabled // true' "$METADATA_FILE")
|
|
50
|
+
THRESHOLD_DAYS=$(jq -r '.archival.threshold_days // 7' "$METADATA_FILE")
|
|
51
|
+
elif command -v node &> /dev/null; then
|
|
52
|
+
ENABLED=$(node -pe "JSON.parse(require('fs').readFileSync('$METADATA_FILE', 'utf8')).archival?.enabled ?? true")
|
|
53
|
+
THRESHOLD_DAYS=$(node -pe "JSON.parse(require('fs').readFileSync('$METADATA_FILE', 'utf8')).archival?.threshold_days ?? 7")
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if [[ "$ENABLED" != "true" ]]; then
|
|
58
|
+
echo -e "${BLUE}Auto-archival is disabled${NC}"
|
|
59
|
+
exit 0
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
echo -e "${BLUE}Starting auto-archival (threshold: $THRESHOLD_DAYS days)...${NC}"
|
|
63
|
+
|
|
64
|
+
# Create archive directory if needed
|
|
65
|
+
mkdir -p "$ARCHIVE_DIR"
|
|
66
|
+
|
|
67
|
+
# Calculate cutoff date (threshold days ago)
|
|
68
|
+
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
69
|
+
# macOS
|
|
70
|
+
CUTOFF_DATE=$(date -v-${THRESHOLD_DAYS}d -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
|
71
|
+
else
|
|
72
|
+
# Linux
|
|
73
|
+
CUTOFF_DATE=$(date -u -d "$THRESHOLD_DAYS days ago" +"%Y-%m-%dT%H:%M:%S.000Z")
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
echo -e "${BLUE}Cutoff date: $CUTOFF_DATE${NC}"
|
|
77
|
+
|
|
78
|
+
# Archive using Node.js (more reliable for JSON manipulation)
|
|
79
|
+
if command -v node &> /dev/null; then
|
|
80
|
+
STATUS_FILE="$STATUS_FILE" ARCHIVE_DIR="$ARCHIVE_DIR" CUTOFF_DATE="$CUTOFF_DATE" node <<'EOF'
|
|
81
|
+
const fs = require('fs');
|
|
82
|
+
const path = require('path');
|
|
83
|
+
|
|
84
|
+
const statusFile = process.env.STATUS_FILE;
|
|
85
|
+
const archiveDir = process.env.ARCHIVE_DIR;
|
|
86
|
+
const cutoffDate = process.env.CUTOFF_DATE;
|
|
87
|
+
|
|
88
|
+
// Read status.json
|
|
89
|
+
const status = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
|
|
90
|
+
const stories = status.stories || {};
|
|
91
|
+
|
|
92
|
+
// Find stories to archive
|
|
93
|
+
const toArchive = {};
|
|
94
|
+
const toKeep = {};
|
|
95
|
+
let archivedCount = 0;
|
|
96
|
+
|
|
97
|
+
for (const [storyId, story] of Object.entries(stories)) {
|
|
98
|
+
if (story.status === 'completed' && story.completed_at) {
|
|
99
|
+
if (story.completed_at < cutoffDate) {
|
|
100
|
+
toArchive[storyId] = story;
|
|
101
|
+
archivedCount++;
|
|
102
|
+
} else {
|
|
103
|
+
toKeep[storyId] = story;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
toKeep[storyId] = story;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (archivedCount === 0) {
|
|
111
|
+
console.log('\x1b[33mNo stories to archive\x1b[0m');
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Group archived stories by month
|
|
116
|
+
const byMonth = {};
|
|
117
|
+
for (const [storyId, story] of Object.entries(toArchive)) {
|
|
118
|
+
const date = new Date(story.completed_at);
|
|
119
|
+
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
120
|
+
|
|
121
|
+
if (!byMonth[monthKey]) {
|
|
122
|
+
byMonth[monthKey] = {
|
|
123
|
+
month: monthKey,
|
|
124
|
+
archived_at: new Date().toISOString(),
|
|
125
|
+
stories: {}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
byMonth[monthKey].stories[storyId] = story;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Write archive files
|
|
133
|
+
for (const [monthKey, archiveData] of Object.entries(byMonth)) {
|
|
134
|
+
const archiveFile = path.join(archiveDir, `${monthKey}.json`);
|
|
135
|
+
|
|
136
|
+
// Merge with existing archive if it exists
|
|
137
|
+
if (fs.existsSync(archiveFile)) {
|
|
138
|
+
const existing = JSON.parse(fs.readFileSync(archiveFile, 'utf8'));
|
|
139
|
+
archiveData.stories = { ...existing.stories, ...archiveData.stories };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fs.writeFileSync(archiveFile, JSON.stringify(archiveData, null, 2));
|
|
143
|
+
const count = Object.keys(archiveData.stories).length;
|
|
144
|
+
console.log(`\x1b[32m✓ Archived ${count} stories to ${monthKey}.json\x1b[0m`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Update status.json
|
|
148
|
+
status.stories = toKeep;
|
|
149
|
+
status.updated = new Date().toISOString();
|
|
150
|
+
fs.writeFileSync(statusFile, JSON.stringify(status, null, 2));
|
|
151
|
+
|
|
152
|
+
console.log(`\x1b[32m✓ Removed ${archivedCount} archived stories from status.json\x1b[0m`);
|
|
153
|
+
console.log(`\x1b[34mStories remaining: ${Object.keys(toKeep).length}\x1b[0m`);
|
|
154
|
+
EOF
|
|
155
|
+
|
|
156
|
+
echo -e "${GREEN}Auto-archival complete!${NC}"
|
|
157
|
+
else
|
|
158
|
+
echo -e "${RED}Error: Node.js not found. Cannot perform archival.${NC}"
|
|
159
|
+
exit 1
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
exit 0
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* clear-active-command.js - Clears active_commands on session start
|
|
4
|
+
*
|
|
5
|
+
* This script runs on SessionStart to reset the active_commands array
|
|
6
|
+
* in session-state.json. This ensures that if a user starts a new chat
|
|
7
|
+
* without running a command like /babysit, they won't get stale command
|
|
8
|
+
* rules in their PreCompact output.
|
|
9
|
+
*
|
|
10
|
+
* Usage: Called automatically by SessionStart hook
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
function clearActiveCommands() {
|
|
17
|
+
const sessionStatePath = path.join(process.cwd(), 'docs/09-agents/session-state.json');
|
|
18
|
+
|
|
19
|
+
// Skip if session-state.json doesn't exist
|
|
20
|
+
if (!fs.existsSync(sessionStatePath)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const sessionState = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
26
|
+
|
|
27
|
+
// Only update if active_commands has items
|
|
28
|
+
if (sessionState.active_commands && sessionState.active_commands.length > 0) {
|
|
29
|
+
sessionState.active_commands = [];
|
|
30
|
+
fs.writeFileSync(sessionStatePath, JSON.stringify(sessionState, null, 2) + '\n', 'utf8');
|
|
31
|
+
console.log('Cleared active_commands from previous session');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Migration: also clear old active_command field if present
|
|
35
|
+
if (sessionState.active_command !== undefined) {
|
|
36
|
+
delete sessionState.active_command;
|
|
37
|
+
fs.writeFileSync(sessionStatePath, JSON.stringify(sessionState, null, 2) + '\n', 'utf8');
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
// Silently ignore errors - don't break session start
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (require.main === module) {
|
|
45
|
+
clearActiveCommands();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { clearActiveCommands };
|