agileflow 2.44.0 → 2.46.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.
@@ -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 };