agileflow 2.84.1 → 2.85.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.
@@ -21,7 +21,10 @@ const { execSync } = require('child_process');
21
21
  const { c: C, box } = require('../lib/colors');
22
22
  const { isValidCommandName } = require('../lib/validate');
23
23
 
24
- const DISPLAY_LIMIT = 30000; // Claude Code's Bash tool display limit
24
+ // Claude Code's Bash tool truncates around 30K chars, but ANSI codes and
25
+ // box-drawing characters (╭╮╰╯─│) are multi-byte UTF-8, so we need buffer.
26
+ // Summary table should be the LAST thing visible before truncation.
27
+ const DISPLAY_LIMIT = 29200;
25
28
 
26
29
  // Optional: Register command for PreCompact context preservation
27
30
  const commandName = process.argv[2];
@@ -300,8 +303,6 @@ function generateSummary() {
300
303
  );
301
304
 
302
305
  summary += bottomBorder;
303
- summary += '\n';
304
- summary += `${C.dim}Full context continues below (Claude sees all)...${C.reset}\n\n`;
305
306
 
306
307
  return summary;
307
308
  }
@@ -706,11 +707,11 @@ if (fullContent.length <= cutoffPoint) {
706
707
  console.log(fullContent);
707
708
  console.log(summary);
708
709
  } else {
709
- // Split: content before cutoff + summary + content after cutoff
710
+ // Output content up to cutoff, then summary as the LAST visible thing.
711
+ // Don't output contentAfter - it would bleed into visible area before truncation,
712
+ // and Claude only sees ~30K chars from Bash anyway.
710
713
  const contentBefore = fullContent.substring(0, cutoffPoint);
711
- const contentAfter = fullContent.substring(cutoffPoint);
712
714
 
713
715
  console.log(contentBefore);
714
716
  console.log(summary);
715
- console.log(contentAfter);
716
717
  }
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * session-boundary.js - PreToolUse hook for Edit/Write session isolation
4
+ *
5
+ * Prevents Claude from editing files outside the active session directory.
6
+ * Used with PreToolUse:Edit and PreToolUse:Write hooks.
7
+ *
8
+ * Exit codes:
9
+ * 0 = Allow the operation
10
+ * 2 = Block with message (shown to Claude)
11
+ *
12
+ * Input: JSON on stdin with tool_input.file_path
13
+ * Output: Error message to stderr if blocking
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ // Inline colors (no external dependency)
20
+ const c = {
21
+ coral: '\x1b[38;5;203m',
22
+ dim: '\x1b[2m',
23
+ reset: '\x1b[0m',
24
+ };
25
+
26
+ const STDIN_TIMEOUT_MS = 4000;
27
+
28
+ // Find project root by looking for .agileflow directory
29
+ function findProjectRoot() {
30
+ let dir = process.cwd();
31
+ while (dir !== '/') {
32
+ if (fs.existsSync(path.join(dir, '.agileflow'))) {
33
+ return dir;
34
+ }
35
+ if (fs.existsSync(path.join(dir, 'docs', '09-agents'))) {
36
+ return dir;
37
+ }
38
+ dir = path.dirname(dir);
39
+ }
40
+ return process.cwd();
41
+ }
42
+
43
+ const ROOT = findProjectRoot();
44
+ const SESSION_STATE_PATH = path.join(ROOT, 'docs', '09-agents', 'session-state.json');
45
+
46
+ // Get active session from session-state.json
47
+ function getActiveSession() {
48
+ try {
49
+ if (!fs.existsSync(SESSION_STATE_PATH)) {
50
+ return null;
51
+ }
52
+ const data = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
53
+ return data.active_session || null;
54
+ } catch (e) {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ // Check if filePath is inside sessionPath
60
+ function isInsideSession(filePath, sessionPath) {
61
+ const normalizedFile = path.resolve(filePath);
62
+ const normalizedSession = path.resolve(sessionPath);
63
+
64
+ return normalizedFile.startsWith(normalizedSession + path.sep) ||
65
+ normalizedFile === normalizedSession;
66
+ }
67
+
68
+ // Output blocked message
69
+ function outputBlocked(filePath, activeSession) {
70
+ const sessionName = activeSession.nickname
71
+ ? `"${activeSession.nickname}"`
72
+ : `Session ${activeSession.id}`;
73
+
74
+ console.error(`${c.coral}[SESSION BOUNDARY]${c.reset} Edit blocked`);
75
+ console.error(`${c.dim}File: ${filePath}${c.reset}`);
76
+ console.error(`${c.dim}Active session: ${sessionName} (${activeSession.path})${c.reset}`);
77
+ console.error('');
78
+ console.error(`${c.dim}The file is outside the active session directory.${c.reset}`);
79
+ console.error(`${c.dim}Use /agileflow:session:resume to switch sessions first.${c.reset}`);
80
+ }
81
+
82
+ // Main logic - run with stdin events (async)
83
+ function main() {
84
+ let inputData = '';
85
+
86
+ process.stdin.setEncoding('utf8');
87
+
88
+ process.stdin.on('data', chunk => {
89
+ inputData += chunk;
90
+ });
91
+
92
+ process.stdin.on('end', () => {
93
+ try {
94
+ // Parse tool input from Claude Code
95
+ const hookData = JSON.parse(inputData);
96
+ const filePath = hookData?.tool_input?.file_path;
97
+
98
+ if (!filePath) {
99
+ // No file path in input - allow
100
+ process.exit(0);
101
+ }
102
+
103
+ // Get active session
104
+ const activeSession = getActiveSession();
105
+
106
+ if (!activeSession || !activeSession.path) {
107
+ // No active session set - allow all (normal behavior)
108
+ process.exit(0);
109
+ }
110
+
111
+ // Check if file is inside active session
112
+ if (isInsideSession(filePath, activeSession.path)) {
113
+ // File is inside active session - allow
114
+ process.exit(0);
115
+ }
116
+
117
+ // File is OUTSIDE active session - BLOCK
118
+ outputBlocked(filePath, activeSession);
119
+ process.exit(2);
120
+
121
+ } catch (e) {
122
+ // Parse error or other issue - fail open
123
+ process.exit(0);
124
+ }
125
+ });
126
+
127
+ // Handle no stdin (direct invocation)
128
+ process.stdin.on('error', () => {
129
+ process.exit(0);
130
+ });
131
+
132
+ // Set timeout to prevent hanging
133
+ setTimeout(() => {
134
+ process.exit(0);
135
+ }, STDIN_TIMEOUT_MS);
136
+ }
137
+
138
+ main();
@@ -786,6 +786,79 @@ function main() {
786
786
  break;
787
787
  }
788
788
 
789
+ // PERFORMANCE: Combined command for welcome script (saves ~200ms from 3 subprocess calls)
790
+ case 'full-status': {
791
+ const nickname = args[1] || null;
792
+ const cwd = process.cwd();
793
+
794
+ // Register in single pass (combines register + count + status)
795
+ const registry = loadRegistry();
796
+ const cleaned = cleanupStaleLocks(registry);
797
+ const branch = getCurrentBranch();
798
+ const story = getCurrentStory();
799
+ const pid = process.ppid || process.pid;
800
+
801
+ // Find or create session
802
+ let sessionId = null;
803
+ let isNew = false;
804
+ for (const [id, session] of Object.entries(registry.sessions)) {
805
+ if (session.path === cwd) {
806
+ sessionId = id;
807
+ break;
808
+ }
809
+ }
810
+
811
+ if (sessionId) {
812
+ // Update existing
813
+ registry.sessions[sessionId].branch = branch;
814
+ registry.sessions[sessionId].story = story ? story.id : null;
815
+ registry.sessions[sessionId].last_active = new Date().toISOString();
816
+ if (nickname) registry.sessions[sessionId].nickname = nickname;
817
+ writeLock(sessionId, pid);
818
+ } else {
819
+ // Create new
820
+ sessionId = String(registry.next_id);
821
+ registry.next_id++;
822
+ registry.sessions[sessionId] = {
823
+ path: cwd,
824
+ branch,
825
+ story: story ? story.id : null,
826
+ nickname: nickname || null,
827
+ created: new Date().toISOString(),
828
+ last_active: new Date().toISOString(),
829
+ is_main: cwd === ROOT,
830
+ };
831
+ writeLock(sessionId, pid);
832
+ isNew = true;
833
+ }
834
+ saveRegistry(registry);
835
+
836
+ // Build session list and counts
837
+ const sessions = [];
838
+ let otherActive = 0;
839
+ for (const [id, session] of Object.entries(registry.sessions)) {
840
+ const active = isSessionActive(id);
841
+ const isCurrent = session.path === cwd;
842
+ sessions.push({ id, ...session, active, current: isCurrent });
843
+ if (active && !isCurrent) otherActive++;
844
+ }
845
+
846
+ const current = sessions.find(s => s.current) || null;
847
+
848
+ console.log(
849
+ JSON.stringify({
850
+ registered: true,
851
+ id: sessionId,
852
+ isNew,
853
+ current,
854
+ otherActive,
855
+ total: sessions.length,
856
+ cleaned,
857
+ })
858
+ );
859
+ break;
860
+ }
861
+
789
862
  case 'check-merge': {
790
863
  const sessionId = args[1];
791
864
  if (!sessionId) {
@@ -888,6 +961,29 @@ function main() {
888
961
  break;
889
962
  }
890
963
 
964
+ case 'switch': {
965
+ const sessionIdOrNickname = args[1];
966
+ if (!sessionIdOrNickname) {
967
+ console.log(JSON.stringify({ success: false, error: 'Session ID or nickname required' }));
968
+ return;
969
+ }
970
+ const result = switchSession(sessionIdOrNickname);
971
+ console.log(JSON.stringify(result, null, 2));
972
+ break;
973
+ }
974
+
975
+ case 'active': {
976
+ const result = getActiveSession();
977
+ console.log(JSON.stringify(result, null, 2));
978
+ break;
979
+ }
980
+
981
+ case 'clear-active': {
982
+ const result = clearActiveSession();
983
+ console.log(JSON.stringify(result));
984
+ break;
985
+ }
986
+
891
987
  case 'help':
892
988
  default:
893
989
  console.log(`
@@ -901,6 +997,10 @@ ${c.cyan}Commands:${c.reset}
901
997
  count Count other active sessions
902
998
  delete <id> [--remove-worktree] Delete session
903
999
  status Get current session status
1000
+ full-status Combined register+count+status (optimized)
1001
+ switch <id|nickname> Switch active session context (for /add-dir)
1002
+ active Get currently switched session (if any)
1003
+ clear-active Clear switched session (back to main)
904
1004
  check-merge <id> Check if session is mergeable to main
905
1005
  merge-preview <id> Preview commits/files to be merged
906
1006
  integrate <id> [opts] Merge session to main and cleanup
@@ -1392,6 +1492,126 @@ function getMergeHistory() {
1392
1492
  }
1393
1493
  }
1394
1494
 
1495
+ // Session state file path
1496
+ const SESSION_STATE_PATH = path.join(ROOT, 'docs', '09-agents', 'session-state.json');
1497
+
1498
+ /**
1499
+ * Switch active session context (for use with /add-dir).
1500
+ * Updates session-state.json with active_session info.
1501
+ *
1502
+ * @param {string} sessionIdOrNickname - Session ID or nickname to switch to
1503
+ * @returns {{ success: boolean, session?: object, path?: string, error?: string }}
1504
+ */
1505
+ function switchSession(sessionIdOrNickname) {
1506
+ const registry = loadRegistry();
1507
+
1508
+ // Find session by ID or nickname
1509
+ let targetSession = null;
1510
+ let targetId = null;
1511
+
1512
+ for (const [id, session] of Object.entries(registry.sessions)) {
1513
+ if (id === sessionIdOrNickname || session.nickname === sessionIdOrNickname) {
1514
+ targetSession = session;
1515
+ targetId = id;
1516
+ break;
1517
+ }
1518
+ }
1519
+
1520
+ if (!targetSession) {
1521
+ return { success: false, error: `Session "${sessionIdOrNickname}" not found` };
1522
+ }
1523
+
1524
+ // Verify the session path exists
1525
+ if (!fs.existsSync(targetSession.path)) {
1526
+ return {
1527
+ success: false,
1528
+ error: `Session directory does not exist: ${targetSession.path}`,
1529
+ };
1530
+ }
1531
+
1532
+ // Load or create session-state.json
1533
+ let sessionState = {};
1534
+ if (fs.existsSync(SESSION_STATE_PATH)) {
1535
+ try {
1536
+ sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
1537
+ } catch (e) {
1538
+ // Start fresh
1539
+ }
1540
+ }
1541
+
1542
+ // Update active_session
1543
+ sessionState.active_session = {
1544
+ id: targetId,
1545
+ nickname: targetSession.nickname,
1546
+ path: targetSession.path,
1547
+ branch: targetSession.branch,
1548
+ switched_at: new Date().toISOString(),
1549
+ original_cwd: ROOT,
1550
+ };
1551
+
1552
+ // Save session-state.json
1553
+ const stateDir = path.dirname(SESSION_STATE_PATH);
1554
+ if (!fs.existsSync(stateDir)) {
1555
+ fs.mkdirSync(stateDir, { recursive: true });
1556
+ }
1557
+ fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(sessionState, null, 2) + '\n');
1558
+
1559
+ // Update session last_active
1560
+ registry.sessions[targetId].last_active = new Date().toISOString();
1561
+ saveRegistry(registry);
1562
+
1563
+ return {
1564
+ success: true,
1565
+ session: {
1566
+ id: targetId,
1567
+ nickname: targetSession.nickname,
1568
+ path: targetSession.path,
1569
+ branch: targetSession.branch,
1570
+ },
1571
+ path: targetSession.path,
1572
+ addDirCommand: `/add-dir ${targetSession.path}`,
1573
+ };
1574
+ }
1575
+
1576
+ /**
1577
+ * Clear active session (switch back to main/original).
1578
+ * @returns {{ success: boolean }}
1579
+ */
1580
+ function clearActiveSession() {
1581
+ if (!fs.existsSync(SESSION_STATE_PATH)) {
1582
+ return { success: true };
1583
+ }
1584
+
1585
+ try {
1586
+ const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
1587
+ delete sessionState.active_session;
1588
+ fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(sessionState, null, 2) + '\n');
1589
+ return { success: true };
1590
+ } catch (e) {
1591
+ return { success: false, error: e.message };
1592
+ }
1593
+ }
1594
+
1595
+ /**
1596
+ * Get current active session (if switched).
1597
+ * @returns {{ active: boolean, session?: object }}
1598
+ */
1599
+ function getActiveSession() {
1600
+ if (!fs.existsSync(SESSION_STATE_PATH)) {
1601
+ return { active: false };
1602
+ }
1603
+
1604
+ try {
1605
+ const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
1606
+ if (sessionState.active_session) {
1607
+ return { active: true, session: sessionState.active_session };
1608
+ }
1609
+ return { active: false };
1610
+ } catch (e) {
1611
+ return { active: false };
1612
+ }
1613
+ }
1614
+
1395
1615
  // Export for use as module
1396
1616
  module.exports = {
1397
1617
  loadRegistry,
@@ -1415,6 +1635,10 @@ module.exports = {
1415
1635
  categorizeFile,
1416
1636
  getMergeStrategy,
1417
1637
  getMergeHistory,
1638
+ // Session switching
1639
+ switchSession,
1640
+ clearActiveSession,
1641
+ getActiveSession,
1418
1642
  };
1419
1643
 
1420
1644
  // Run CLI if executed directly
@@ -133,26 +133,38 @@ Then create with specified branch:
133
133
  node .agileflow/scripts/session-manager.js create --branch {branch_name}
134
134
  ```
135
135
 
136
- ### Step 5: Display Success Message
136
+ ### Step 5: Display Success with Switch Command
137
137
 
138
- Show the created session details and the command to start working:
138
+ After session creation succeeds:
139
139
 
140
+ 1. First, activate boundary protection for the new session:
141
+ ```bash
142
+ node .agileflow/scripts/session-manager.js switch {new_session_id}
140
143
  ```
141
- ✓ Created Session {id} "{nickname}"
142
144
 
143
- Workspace: ../project-{name}
144
- Branch: session-{id}-{name}
145
+ 2. Then show the `/add-dir` command for the user to switch:
145
146
 
146
- ┌─────────────────────────────────────────────────────────┐
147
- To start working in this session, run: │
148
- │ │
149
- │ cd ../project-{name} && claude │
150
-
151
- └─────────────────────────────────────────────────────────┘
147
+ ```
148
+ Created Session {id} "{nickname}"
149
+
150
+ ┌───────────┬────────────────────────────────────────────┐
151
+ Session {id} "{nickname}" │
152
+ │ Workspace │ {path} │
153
+ │ Branch │ {branch} │
154
+ └───────────┴────────────────────────────────────────────┘
155
+
156
+ To switch to this session, run:
157
+
158
+ /add-dir {path}
152
159
 
153
- 💡 Tip: Use /agileflow:session:resume to see all sessions
160
+ 💡 Use /agileflow:session:resume to list all sessions
154
161
  ```
155
162
 
163
+ **WHY /add-dir instead of cd && claude:**
164
+ - Stays in the same terminal and conversation
165
+ - One short command to type
166
+ - Immediately enables file access to the new session directory
167
+
156
168
  ## Error Handling
157
169
 
158
170
  - **Directory exists**: Suggest different name or manual cleanup
@@ -228,21 +240,26 @@ If user selects "Auto-create":
228
240
  node .agileflow/scripts/session-manager.js create
229
241
  ```
230
242
 
231
- Display:
243
+ Parse JSON result, then activate boundary protection:
244
+ ```bash
245
+ node .agileflow/scripts/session-manager.js switch {new_id}
232
246
  ```
233
- ✓ Created Session {id}
234
247
 
235
- Workspace: ../project-{id}
236
- Branch: session-{id}
248
+ Then display:
249
+ ```
250
+ ✅ Created Session {id}
237
251
 
238
- ┌─────────────────────────────────────────────────────────┐
239
- To start working in this session, run:
240
-
241
- cd ../project-{id} && claude
242
- │ │
243
- └─────────────────────────────────────────────────────────┘
252
+ ┌───────────┬────────────────────────────────────────────┐
253
+ Session │ {id}
254
+ Workspace {path} │
255
+ Branch │ {branch}
256
+ └───────────┴────────────────────────────────────────────┘
257
+
258
+ To switch to this session, run:
244
259
 
245
- 💡 Tip: Use /agileflow:session:resume to see all sessions
260
+ /add-dir {path}
261
+
262
+ 💡 Use /agileflow:session:resume to list all sessions
246
263
  ```
247
264
 
248
265
  ---
@@ -274,21 +291,26 @@ Then create:
274
291
  node .agileflow/scripts/session-manager.js create --nickname {name}
275
292
  ```
276
293
 
277
- Display:
294
+ Parse JSON result, then activate boundary protection:
295
+ ```bash
296
+ node .agileflow/scripts/session-manager.js switch {new_id}
278
297
  ```
279
- ✓ Created Session {id} "{name}"
280
298
 
281
- Workspace: ../project-{name}
282
- Branch: session-{id}-{name}
299
+ Then display:
300
+ ```
301
+ ✅ Created Session {id} "{name}"
283
302
 
284
- ┌─────────────────────────────────────────────────────────┐
285
- To start working in this session, run:
286
-
287
- cd ../project-{name} && claude
288
- │ │
289
- └─────────────────────────────────────────────────────────┘
303
+ ┌───────────┬────────────────────────────────────────────┐
304
+ Session │ {id} "{name}"
305
+ Workspace {path} │
306
+ Branch │ {branch}
307
+ └───────────┴────────────────────────────────────────────┘
308
+
309
+ To switch to this session, run:
310
+
311
+ /add-dir {path}
290
312
 
291
- 💡 Tip: Use /agileflow:session:resume to see all sessions
313
+ 💡 Use /agileflow:session:resume to list all sessions
292
314
  ```
293
315
 
294
316
  ---
@@ -327,7 +349,12 @@ git branch --format='%(refname:short)'
327
349
  node .agileflow/scripts/session-manager.js create --branch {branch_name}
328
350
  ```
329
351
 
330
- Display success as above.
352
+ 5. Parse JSON result, then activate boundary protection:
353
+ ```bash
354
+ node .agileflow/scripts/session-manager.js switch {new_id}
355
+ ```
356
+
357
+ 6. Display success as above with `/add-dir` command.
331
358
 
332
359
  ---
333
360
 
@@ -365,21 +392,23 @@ Try running: git status
365
392
 
366
393
  All three options show same format:
367
394
  ```
368
- Created Session {id} ["{nickname}" OR empty]
395
+ Created Session {id} ["{nickname}" OR empty]
369
396
 
370
- Workspace: ../project-{path}
371
- Branch: {branch_name}
397
+ ┌───────────┬────────────────────────────────────────────┐
398
+ │ Session │ {id} ["{nickname}" or empty] │
399
+ │ Workspace │ {path} │
400
+ │ Branch │ {branch} │
401
+ └───────────┴────────────────────────────────────────────┘
372
402
 
373
- ┌─────────────────────────────────────────────────────────┐
374
- │ To start working in this session, run: │
375
- │ │
376
- │ {cd_command} │
377
- │ │
378
- └─────────────────────────────────────────────────────────┘
403
+ To switch to this session, run:
379
404
 
380
- 💡 Tip: Use /agileflow:session:resume to see all sessions
405
+ /add-dir {path}
406
+
407
+ 💡 Use /agileflow:session:resume to list all sessions
381
408
  ```
382
409
 
410
+ **Use /add-dir instead of cd && claude** - stays in same terminal/conversation.
411
+
383
412
  ---
384
413
 
385
414
  ### KEY FILES TO REMEMBER
@@ -400,7 +429,8 @@ All three options show same format:
400
429
  4. **User selects** → Option 1, 2, or 3
401
430
  5. **Handle selection** → Different flow for each
402
431
  6. **Create session** → Call manager script
403
- 7. **Show success** → Display cd command
432
+ 7. **Activate boundary** → `session-manager.js switch {new_id}`
433
+ 8. **Show success** → Display `/add-dir {path}` command for user to run
404
434
 
405
435
  ---
406
436
 
@@ -420,7 +450,7 @@ All three options show same format:
420
450
  ❌ Don't show more/fewer than 3 initial options
421
451
  ❌ Don't create session without explicit user choice
422
452
  ❌ Don't skip error handling (directory exists, branch conflict)
423
- ❌ Don't forget cd command in success message
453
+ ❌ Don't show old "cd && claude" command - use /add-dir instead
424
454
  ❌ Show different success formats for different methods
425
455
 
426
456
  ### DO THESE INSTEAD
@@ -429,7 +459,7 @@ All three options show same format:
429
459
  ✅ Always show exactly 3 options
430
460
  ✅ Wait for user to select before creating
431
461
  ✅ Handle all error cases gracefully
432
- Always show cd command in success
462
+ Show `/add-dir {path}` command for user to switch
433
463
  ✅ Use consistent success format
434
464
 
435
465
  ---
@@ -442,7 +472,8 @@ All three options show same format:
442
472
  - Each option leads to different flow
443
473
  - Use AskUserQuestion for user selections
444
474
  - Handle all error cases (directory, branch, git)
445
- - Return success with cd command
475
+ - **Run `session-manager.js switch {new_id}` AFTER creating session** (enables boundary protection)
476
+ - Show `/add-dir {path}` command for user to switch (NOT cd && claude)
446
477
  - Show tip to use /agileflow:session:resume
447
478
 
448
479
  <!-- COMPACT_SUMMARY_END -->