agileflow 2.50.0 → 2.55.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.
Files changed (87) hide show
  1. package/README.md +82 -460
  2. package/package.json +18 -3
  3. package/scripts/agileflow-configure.js +134 -63
  4. package/scripts/agileflow-welcome.js +161 -31
  5. package/scripts/generators/agent-registry.js +2 -2
  6. package/scripts/generators/command-registry.js +6 -6
  7. package/scripts/generators/index.js +2 -6
  8. package/scripts/generators/inject-babysit.js +9 -2
  9. package/scripts/generators/inject-help.js +3 -1
  10. package/scripts/generators/inject-readme.js +7 -3
  11. package/scripts/generators/skill-registry.js +5 -5
  12. package/scripts/get-env.js +13 -12
  13. package/scripts/obtain-context.js +396 -185
  14. package/scripts/session-coordinator.sh +232 -0
  15. package/scripts/session-manager.js +512 -0
  16. package/src/core/agents/orchestrator.md +275 -0
  17. package/src/core/commands/adr.md +38 -16
  18. package/src/core/commands/agent.md +39 -22
  19. package/src/core/commands/assign.md +17 -0
  20. package/src/core/commands/auto.md +60 -46
  21. package/src/core/commands/babysit.md +302 -637
  22. package/src/core/commands/baseline.md +20 -0
  23. package/src/core/commands/blockers.md +33 -48
  24. package/src/core/commands/board.md +19 -0
  25. package/src/core/commands/changelog.md +20 -0
  26. package/src/core/commands/ci.md +17 -0
  27. package/src/core/commands/context.md +43 -40
  28. package/src/core/commands/debt.md +76 -45
  29. package/src/core/commands/deploy.md +20 -0
  30. package/src/core/commands/deps.md +40 -46
  31. package/src/core/commands/diagnose.md +24 -18
  32. package/src/core/commands/docs.md +18 -0
  33. package/src/core/commands/epic.md +31 -0
  34. package/src/core/commands/feedback.md +33 -21
  35. package/src/core/commands/handoff.md +29 -0
  36. package/src/core/commands/help.md +16 -7
  37. package/src/core/commands/impact.md +31 -61
  38. package/src/core/commands/metrics.md +17 -35
  39. package/src/core/commands/packages.md +21 -0
  40. package/src/core/commands/pr.md +15 -0
  41. package/src/core/commands/readme-sync.md +42 -9
  42. package/src/core/commands/research.md +58 -11
  43. package/src/core/commands/retro.md +42 -50
  44. package/src/core/commands/review.md +22 -27
  45. package/src/core/commands/session/end.md +53 -297
  46. package/src/core/commands/session/history.md +38 -257
  47. package/src/core/commands/session/init.md +44 -446
  48. package/src/core/commands/session/new.md +152 -0
  49. package/src/core/commands/session/resume.md +51 -447
  50. package/src/core/commands/session/status.md +32 -244
  51. package/src/core/commands/sprint.md +33 -0
  52. package/src/core/commands/status.md +18 -0
  53. package/src/core/commands/story-validate.md +32 -0
  54. package/src/core/commands/story.md +21 -6
  55. package/src/core/commands/template.md +18 -0
  56. package/src/core/commands/tests.md +22 -0
  57. package/src/core/commands/update.md +72 -58
  58. package/src/core/commands/validate-expertise.md +25 -37
  59. package/src/core/commands/velocity.md +33 -74
  60. package/src/core/commands/verify.md +16 -0
  61. package/src/core/experts/documentation/expertise.yaml +16 -2
  62. package/src/core/skills/agileflow-retro-facilitator/SKILL.md +57 -219
  63. package/src/core/skills/agileflow-retro-facilitator/cookbook/4ls.md +86 -0
  64. package/src/core/skills/agileflow-retro-facilitator/cookbook/glad-sad-mad.md +79 -0
  65. package/src/core/skills/agileflow-retro-facilitator/cookbook/start-stop-continue.md +142 -0
  66. package/src/core/skills/agileflow-retro-facilitator/prompts/action-items.md +83 -0
  67. package/src/core/skills/writing-skills/SKILL.md +352 -0
  68. package/src/core/skills/writing-skills/testing-skills-with-subagents.md +232 -0
  69. package/tools/cli/agileflow-cli.js +4 -2
  70. package/tools/cli/commands/config.js +20 -13
  71. package/tools/cli/commands/doctor.js +25 -9
  72. package/tools/cli/commands/list.js +10 -6
  73. package/tools/cli/commands/setup.js +54 -3
  74. package/tools/cli/commands/status.js +6 -8
  75. package/tools/cli/commands/uninstall.js +5 -5
  76. package/tools/cli/commands/update.js +51 -7
  77. package/tools/cli/installers/core/installer.js +8 -4
  78. package/tools/cli/installers/ide/_base-ide.js +3 -1
  79. package/tools/cli/installers/ide/claude-code.js +3 -7
  80. package/tools/cli/installers/ide/codex.js +440 -0
  81. package/tools/cli/installers/ide/manager.js +2 -6
  82. package/tools/cli/lib/content-injector.js +3 -3
  83. package/tools/cli/lib/docs-setup.js +3 -2
  84. package/tools/cli/lib/npm-utils.js +3 -3
  85. package/tools/cli/lib/ui.js +7 -7
  86. package/tools/cli/lib/version-checker.js +3 -3
  87. package/tools/postinstall.js +2 -3
@@ -3,19 +3,24 @@
3
3
  * obtain-context.js
4
4
  *
5
5
  * Gathers all project context in a single execution for any AgileFlow command or agent.
6
- * Optionally registers the command/agent for PreCompact context preservation.
7
- * Outputs structured summary to reduce tool calls and startup time.
6
+ *
7
+ * SMART OUTPUT STRATEGY:
8
+ * - Calculates summary character count dynamically
9
+ * - Shows (30K - summary_chars) of full content first
10
+ * - Then shows the summary (so user sees it at their display cutoff)
11
+ * - Then shows rest of full content (for Claude)
8
12
  *
9
13
  * Usage:
10
14
  * node scripts/obtain-context.js # Just gather context
11
15
  * node scripts/obtain-context.js babysit # Gather + register 'babysit'
12
- * node scripts/obtain-context.js mentor # Gather + register 'mentor'
13
16
  */
14
17
 
15
18
  const fs = require('fs');
16
19
  const path = require('path');
17
20
  const { execSync } = require('child_process');
18
21
 
22
+ const DISPLAY_LIMIT = 30000; // Claude Code's Bash tool display limit
23
+
19
24
  // Optional: Register command for PreCompact context preservation
20
25
  const commandName = process.argv[2];
21
26
  if (commandName) {
@@ -23,7 +28,11 @@ if (commandName) {
23
28
  if (fs.existsSync(sessionStatePath)) {
24
29
  try {
25
30
  const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
26
- state.active_command = { name: commandName, activated_at: new Date().toISOString(), state: {} };
31
+ state.active_command = {
32
+ name: commandName,
33
+ activated_at: new Date().toISOString(),
34
+ state: {},
35
+ };
27
36
  fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
28
37
  } catch (e) {
29
38
  // Silently continue if session state can't be updated
@@ -41,6 +50,11 @@ const C = {
41
50
  green: '\x1b[32m',
42
51
  red: '\x1b[31m',
43
52
  magenta: '\x1b[35m',
53
+ blue: '\x1b[34m',
54
+ brightCyan: '\x1b[96m',
55
+ brightYellow: '\x1b[93m',
56
+ brightGreen: '\x1b[92m',
57
+ brand: '\x1b[38;2;232;104;58m', // AgileFlow brand orange
44
58
  };
45
59
 
46
60
  function safeRead(filePath) {
@@ -75,219 +89,416 @@ function safeExec(cmd) {
75
89
  }
76
90
  }
77
91
 
78
- function section(title) {
79
- console.log(`\n${C.cyan}${C.bold}═══ ${title} ═══${C.reset}`);
80
- }
81
-
82
- function subsection(title) {
83
- console.log(`${C.dim}───${C.reset} ${title}`);
84
- }
85
-
86
92
  // ============================================
87
- // MAIN CONTEXT GATHERING
93
+ // GENERATE SUMMARY (calculated first for positioning)
88
94
  // ============================================
89
95
 
90
- const title = commandName ? `AgileFlow Context [${commandName}]` : 'AgileFlow Context';
91
- console.log(`${C.magenta}${C.bold}${title}${C.reset}`);
92
- console.log(`${C.dim}Generated: ${new Date().toISOString()}${C.reset}`);
93
-
94
- // 1. GIT STATUS
95
- section('Git Status');
96
- const branch = safeExec('git branch --show-current') || 'unknown';
97
- const status = safeExec('git status --short') || '';
98
- const statusLines = status.split('\n').filter(Boolean);
99
- const lastCommit = safeExec('git log -1 --format="%h %s"') || 'no commits';
100
-
101
- console.log(`Branch: ${C.green}${branch}${C.reset}`);
102
- console.log(`Last commit: ${C.dim}${lastCommit}${C.reset}`);
103
- if (statusLines.length > 0) {
104
- console.log(`Uncommitted: ${C.yellow}${statusLines.length} file(s)${C.reset}`);
105
- statusLines.slice(0, 10).forEach(line => console.log(` ${C.dim}${line}${C.reset}`));
106
- if (statusLines.length > 10) console.log(` ${C.dim}... and ${statusLines.length - 10} more${C.reset}`);
107
- } else {
108
- console.log(`Uncommitted: ${C.green}clean${C.reset}`);
109
- }
96
+ function generateSummary() {
97
+ // Box drawing characters
98
+ const box = {
99
+ tl: '╭',
100
+ tr: '╮',
101
+ bl: '╰',
102
+ br: '',
103
+ h: '',
104
+ v: '',
105
+ lT: '',
106
+ rT: '┤',
107
+ tT: '┬',
108
+ bT: '┴',
109
+ cross: '┼',
110
+ };
110
111
 
111
- // 2. STATUS.JSON - Stories & Epics
112
- section('Stories & Epics');
113
- const statusJson = safeReadJSON('docs/09-agents/status.json');
114
- if (statusJson) {
115
- // Epics summary
116
- const epics = statusJson.epics || {};
117
- const epicList = Object.entries(epics);
118
- if (epicList.length > 0) {
119
- subsection('Epics');
120
- epicList.forEach(([id, epic]) => {
121
- const statusColor = epic.status === 'complete' ? C.green : epic.status === 'active' ? C.yellow : C.dim;
122
- console.log(` ${id}: ${epic.title} ${statusColor}[${epic.status}]${C.reset}`);
123
- });
112
+ const W = 58; // Total inner width (matches welcome script)
113
+ const L = 20; // Left column width
114
+ const R = W - L - 3; // Right column width (35 chars)
115
+
116
+ // Pad string to length, accounting for ANSI codes
117
+ function pad(str, len) {
118
+ const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
119
+ const diff = len - stripped.length;
120
+ if (diff <= 0) return str;
121
+ return str + ' '.repeat(diff);
124
122
  }
125
123
 
126
- // Stories summary by status
127
- const stories = statusJson.stories || {};
128
- const storyList = Object.entries(stories);
129
- const byStatus = {};
130
- storyList.forEach(([id, story]) => {
131
- const s = story.status || 'unknown';
132
- if (!byStatus[s]) byStatus[s] = [];
133
- byStatus[s].push({ id, ...story });
134
- });
124
+ // Truncate string to max length, respecting ANSI codes
125
+ function truncate(str, maxLen, suffix = '..') {
126
+ const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
127
+ if (stripped.length <= maxLen) return str;
135
128
 
136
- // Priority order for display
137
- const statusOrder = ['in-progress', 'ready', 'blocked', 'draft', 'in-review', 'done'];
138
-
139
- subsection('Stories by Status');
140
- statusOrder.forEach(status => {
141
- if (byStatus[status] && byStatus[status].length > 0) {
142
- const color = status === 'in-progress' ? C.yellow :
143
- status === 'ready' ? C.green :
144
- status === 'blocked' ? C.red :
145
- status === 'done' ? C.dim : C.reset;
146
- console.log(` ${color}${status}${C.reset}: ${byStatus[status].length}`);
147
- byStatus[status].slice(0, 5).forEach(story => {
148
- console.log(` ${C.dim}${story.id}: ${story.title}${C.reset}`);
149
- });
150
- if (byStatus[status].length > 5) {
151
- console.log(` ${C.dim}... and ${byStatus[status].length - 5} more${C.reset}`);
129
+ const targetLen = maxLen - suffix.length;
130
+ let visibleCount = 0;
131
+ let cutIndex = 0;
132
+ let inEscape = false;
133
+
134
+ for (let i = 0; i < str.length; i++) {
135
+ if (str[i] === '\x1b') {
136
+ inEscape = true;
137
+ } else if (inEscape && str[i] === 'm') {
138
+ inEscape = false;
139
+ } else if (!inEscape) {
140
+ visibleCount++;
141
+ if (visibleCount >= targetLen) {
142
+ cutIndex = i + 1;
143
+ break;
144
+ }
152
145
  }
153
146
  }
154
- });
147
+ return str.substring(0, cutIndex) + suffix;
148
+ }
149
+
150
+ // Create a row with auto-truncation
151
+ function row(left, right, leftColor = '', rightColor = '') {
152
+ const leftStr = `${leftColor}${left}${leftColor ? C.reset : ''}`;
153
+ const rightTrunc = truncate(right, R);
154
+ const rightStr = `${rightColor}${rightTrunc}${rightColor ? C.reset : ''}`;
155
+ return `${C.dim}${box.v}${C.reset} ${pad(leftStr, L)} ${C.dim}${box.v}${C.reset} ${pad(rightStr, R)} ${C.dim}${box.v}${C.reset}\n`;
156
+ }
155
157
 
156
- // Show READY stories prominently (these are actionable)
157
- if (byStatus['ready'] && byStatus['ready'].length > 0) {
158
- subsection(`${C.green} Ready to Implement${C.reset}`);
159
- byStatus['ready'].forEach(story => {
160
- console.log(` ${story.id}: ${story.title} (${story.epic || 'no epic'})`);
158
+ const divider = () =>
159
+ `${C.dim}${box.lT}${box.h.repeat(L + 2)}${box.cross}${box.h.repeat(R + 2)}${box.rT}${C.reset}\n`;
160
+ const headerTopBorder = `${C.dim}${box.tl}${box.h.repeat(W + 2)}${box.tr}${C.reset}\n`;
161
+ const headerDivider = `${C.dim}${box.lT}${box.h.repeat(L + 2)}${box.tT}${box.h.repeat(R + 2)}${box.rT}${C.reset}\n`;
162
+ const bottomBorder = `${C.dim}${box.bl}${box.h.repeat(L + 2)}${box.bT}${box.h.repeat(R + 2)}${box.br}${C.reset}\n`;
163
+
164
+ // Gather data
165
+ const branch = safeExec('git branch --show-current') || 'unknown';
166
+ const lastCommitShort = safeExec('git log -1 --format="%h"') || '?';
167
+ const lastCommitMsg = safeExec('git log -1 --format="%s"') || 'no commits';
168
+ const statusLines = (safeExec('git status --short') || '').split('\n').filter(Boolean);
169
+ const statusJson = safeReadJSON('docs/09-agents/status.json');
170
+ const sessionState = safeReadJSON('docs/09-agents/session-state.json');
171
+ const researchFiles = safeLs('docs/10-research')
172
+ .filter(f => f.endsWith('.md') && f !== 'README.md')
173
+ .sort()
174
+ .reverse();
175
+ const epicFiles = safeLs('docs/05-epics').filter(f => f.endsWith('.md') && f !== 'README.md');
176
+
177
+ // Count stories by status
178
+ const byStatus = {};
179
+ const readyStories = [];
180
+ if (statusJson && statusJson.stories) {
181
+ Object.entries(statusJson.stories).forEach(([id, story]) => {
182
+ const s = story.status || 'unknown';
183
+ byStatus[s] = (byStatus[s] || 0) + 1;
184
+ if (s === 'ready') readyStories.push(id);
161
185
  });
162
186
  }
163
- } else {
164
- console.log(`${C.dim}No status.json found${C.reset}`);
187
+
188
+ // Session info
189
+ let sessionDuration = null;
190
+ let currentStory = null;
191
+ if (sessionState && sessionState.current_session && sessionState.current_session.started_at) {
192
+ const started = new Date(sessionState.current_session.started_at);
193
+ sessionDuration = Math.round((Date.now() - started.getTime()) / 60000);
194
+ currentStory = sessionState.current_session.current_story;
195
+ }
196
+
197
+ // Build table
198
+ let summary = '\n';
199
+ summary += headerTopBorder;
200
+
201
+ // Header row (full width, no column divider)
202
+ const title = commandName ? `Context [${commandName}]` : 'Context Summary';
203
+ const branchColor = branch === 'main' ? C.green : branch.startsWith('fix') ? C.red : C.cyan;
204
+ const maxBranchLen = 20;
205
+ const branchDisplay =
206
+ branch.length > maxBranchLen ? branch.substring(0, maxBranchLen - 2) + '..' : branch;
207
+ const header = `${C.brand}${C.bold}${title}${C.reset} ${branchColor}${branchDisplay}${C.reset} ${C.dim}(${lastCommitShort})${C.reset}`;
208
+ summary += `${C.dim}${box.v}${C.reset} ${pad(header, W)} ${C.dim}${box.v}${C.reset}\n`;
209
+
210
+ summary += headerDivider;
211
+
212
+ // Story counts with colorful labels
213
+ summary += row(
214
+ 'In Progress',
215
+ byStatus['in-progress'] ? `${byStatus['in-progress']}` : '0',
216
+ C.yellow,
217
+ byStatus['in-progress'] ? C.brightYellow : C.dim
218
+ );
219
+ summary += row(
220
+ 'Blocked',
221
+ byStatus['blocked'] ? `${byStatus['blocked']}` : '0',
222
+ C.red,
223
+ byStatus['blocked'] ? C.red : C.dim
224
+ );
225
+ summary += row(
226
+ 'Ready',
227
+ byStatus['ready'] ? `${byStatus['ready']}` : '0',
228
+ C.cyan,
229
+ byStatus['ready'] ? C.brightCyan : C.dim
230
+ );
231
+ const completedColor = `${C.bold}${C.green}`;
232
+ summary += row(
233
+ 'Completed',
234
+ byStatus['done'] ? `${byStatus['done']}` : '0',
235
+ completedColor,
236
+ byStatus['done'] ? completedColor : C.dim
237
+ );
238
+
239
+ summary += divider();
240
+
241
+ // Git status
242
+ const uncommittedStatus =
243
+ statusLines.length > 0 ? `${statusLines.length} uncommitted` : '✓ clean';
244
+ summary += row('Git', uncommittedStatus, C.blue, statusLines.length > 0 ? C.yellow : C.green);
245
+
246
+ // Session
247
+ const sessionText = sessionDuration !== null ? `${sessionDuration} min active` : 'no session';
248
+ summary += row('Session', sessionText, C.blue, sessionDuration !== null ? C.brightGreen : C.dim);
249
+
250
+ // Current story
251
+ const storyText = currentStory ? currentStory : 'none';
252
+ summary += row('Working on', storyText, C.blue, currentStory ? C.brightYellow : C.dim);
253
+
254
+ // Ready stories (if any)
255
+ if (readyStories.length > 0) {
256
+ summary += row('⭐ Up Next', readyStories.slice(0, 3).join(', '), C.brightCyan, C.cyan);
257
+ }
258
+
259
+ summary += divider();
260
+
261
+ // Key files
262
+ const keyFileChecks = [
263
+ { path: 'CLAUDE.md', label: 'CLAUDE' },
264
+ { path: 'README.md', label: 'README' },
265
+ { path: 'docs/04-architecture/README.md', label: 'arch' },
266
+ { path: 'docs/02-practices/README.md', label: 'practices' },
267
+ ];
268
+ const keyFileStatus = keyFileChecks
269
+ .map(f => {
270
+ const exists = fs.existsSync(f.path);
271
+ return exists ? `${C.brightGreen}✓${C.reset}${f.label}` : `${C.dim}○${f.label}${C.reset}`;
272
+ })
273
+ .join(' ');
274
+ summary += row('Key files', keyFileStatus, C.magenta, '');
275
+
276
+ // Research
277
+ const researchText = researchFiles.length > 0 ? `${researchFiles.length} notes` : 'none';
278
+ summary += row('Research', researchText, C.magenta, researchFiles.length > 0 ? C.cyan : C.dim);
279
+
280
+ // Epics
281
+ const epicText = epicFiles.length > 0 ? `${epicFiles.length} epics` : 'none';
282
+ summary += row('Epics', epicText, C.magenta, epicFiles.length > 0 ? C.cyan : C.dim);
283
+
284
+ summary += divider();
285
+
286
+ // Last commit
287
+ summary += row(
288
+ 'Last commit',
289
+ `${C.yellow}${lastCommitShort}${C.reset} ${lastCommitMsg}`,
290
+ C.dim,
291
+ ''
292
+ );
293
+
294
+ summary += bottomBorder;
295
+
296
+ summary += `${C.dim}Full context continues below (Claude sees all)...${C.reset}\n\n`;
297
+
298
+ return summary;
165
299
  }
166
300
 
167
- // 3. SESSION STATE
168
- section('Session State');
169
- const sessionState = safeReadJSON('docs/09-agents/session-state.json');
170
- if (sessionState) {
171
- const current = sessionState.current_session;
172
- if (current && current.started_at) {
173
- const started = new Date(current.started_at);
174
- const duration = Math.round((Date.now() - started.getTime()) / 60000);
175
- console.log(`Active session: ${C.green}${duration} min${C.reset}`);
176
- if (current.current_story) {
177
- console.log(`Working on: ${C.yellow}${current.current_story}${C.reset}`);
301
+ // ============================================
302
+ // GENERATE FULL CONTENT
303
+ // ============================================
304
+
305
+ function generateFullContent() {
306
+ let content = '';
307
+
308
+ const title = commandName ? `AgileFlow Context [${commandName}]` : 'AgileFlow Context';
309
+ content += `${C.magenta}${C.bold}${title}${C.reset}\n`;
310
+ content += `${C.dim}Generated: ${new Date().toISOString()}${C.reset}\n`;
311
+
312
+ // 1. GIT STATUS
313
+ content += `\n${C.cyan}${C.bold}═══ Git Status ═══${C.reset}\n`;
314
+ const branch = safeExec('git branch --show-current') || 'unknown';
315
+ const status = safeExec('git status --short') || '';
316
+ const statusLines = status.split('\n').filter(Boolean);
317
+ const lastCommit = safeExec('git log -1 --format="%h %s"') || 'no commits';
318
+
319
+ content += `Branch: ${C.green}${branch}${C.reset}\n`;
320
+ content += `Last commit: ${C.dim}${lastCommit}${C.reset}\n`;
321
+ if (statusLines.length > 0) {
322
+ content += `Uncommitted: ${C.yellow}${statusLines.length} file(s)${C.reset}\n`;
323
+ statusLines.slice(0, 10).forEach(line => (content += ` ${C.dim}${line}${C.reset}\n`));
324
+ if (statusLines.length > 10)
325
+ content += ` ${C.dim}... and ${statusLines.length - 10} more${C.reset}\n`;
326
+ } else {
327
+ content += `Uncommitted: ${C.green}clean${C.reset}\n`;
328
+ }
329
+
330
+ // 2. STATUS.JSON - Full Content
331
+ content += `\n${C.cyan}${C.bold}═══ Status.json (Full Content) ═══${C.reset}\n`;
332
+ const statusJsonPath = 'docs/09-agents/status.json';
333
+ const statusJson = safeReadJSON(statusJsonPath);
334
+
335
+ if (statusJson) {
336
+ content += `${C.dim}${'─'.repeat(50)}${C.reset}\n`;
337
+ content +=
338
+ JSON.stringify(statusJson, null, 2)
339
+ .split('\n')
340
+ .map(l => ` ${l}`)
341
+ .join('\n') + '\n';
342
+ content += `${C.dim}${'─'.repeat(50)}${C.reset}\n`;
343
+ } else {
344
+ content += `${C.dim}No status.json found${C.reset}\n`;
345
+ }
346
+
347
+ // 3. SESSION STATE
348
+ content += `\n${C.cyan}${C.bold}═══ Session State ═══${C.reset}\n`;
349
+ const sessionState = safeReadJSON('docs/09-agents/session-state.json');
350
+ if (sessionState) {
351
+ const current = sessionState.current_session;
352
+ if (current && current.started_at) {
353
+ const started = new Date(current.started_at);
354
+ const duration = Math.round((Date.now() - started.getTime()) / 60000);
355
+ content += `Active session: ${C.green}${duration} min${C.reset}\n`;
356
+ if (current.current_story) {
357
+ content += `Working on: ${C.yellow}${current.current_story}${C.reset}\n`;
358
+ }
359
+ } else {
360
+ content += `${C.dim}No active session${C.reset}\n`;
361
+ }
362
+ if (sessionState.active_command) {
363
+ content += `Active command: ${C.cyan}${sessionState.active_command.name}${C.reset}\n`;
178
364
  }
179
365
  } else {
180
- console.log(`${C.dim}No active session${C.reset}`);
366
+ content += `${C.dim}No session-state.json found${C.reset}\n`;
181
367
  }
182
368
 
183
- const last = sessionState.last_session;
184
- if (last && last.ended_at) {
185
- console.log(`Last session: ${C.dim}${last.ended_at} (${last.duration_minutes || '?'} min)${C.reset}`);
186
- if (last.summary) console.log(` Summary: ${C.dim}${last.summary}${C.reset}`);
369
+ // 4. DOCS STRUCTURE
370
+ content += `\n${C.cyan}${C.bold}═══ Documentation ═══${C.reset}\n`;
371
+ const docsDir = 'docs';
372
+ const docFolders = safeLs(docsDir).filter(f => {
373
+ try {
374
+ return fs.statSync(path.join(docsDir, f)).isDirectory();
375
+ } catch {
376
+ return false;
377
+ }
378
+ });
379
+
380
+ if (docFolders.length > 0) {
381
+ docFolders.forEach(folder => {
382
+ const folderPath = path.join(docsDir, folder);
383
+ const files = safeLs(folderPath);
384
+ const mdFiles = files.filter(f => f.endsWith('.md'));
385
+ const jsonFiles = files.filter(f => f.endsWith('.json') || f.endsWith('.jsonl'));
386
+ const info = [];
387
+ if (mdFiles.length > 0) info.push(`${mdFiles.length} md`);
388
+ if (jsonFiles.length > 0) info.push(`${jsonFiles.length} json`);
389
+ content += ` ${C.dim}${folder}/${C.reset} ${info.length > 0 ? `(${info.join(', ')})` : ''}\n`;
390
+ });
187
391
  }
188
392
 
189
- // Active command (for context preservation)
190
- if (sessionState.active_command) {
191
- console.log(`Active command: ${C.cyan}${sessionState.active_command.name}${C.reset}`);
393
+ // 5. RESEARCH NOTES - List + Full content of most recent
394
+ content += `\n${C.cyan}${C.bold}═══ Research Notes ═══${C.reset}\n`;
395
+ const researchDir = 'docs/10-research';
396
+ const researchFiles = safeLs(researchDir).filter(f => f.endsWith('.md') && f !== 'README.md');
397
+ if (researchFiles.length > 0) {
398
+ researchFiles.sort().reverse();
399
+ content += `${C.dim}───${C.reset} Available Research Notes\n`;
400
+ researchFiles.forEach(file => (content += ` ${C.dim}${file}${C.reset}\n`));
401
+
402
+ const mostRecentFile = researchFiles[0];
403
+ const mostRecentPath = path.join(researchDir, mostRecentFile);
404
+ const mostRecentContent = safeRead(mostRecentPath);
405
+
406
+ if (mostRecentContent) {
407
+ content += `\n${C.green}📄 Most Recent: ${mostRecentFile}${C.reset}\n`;
408
+ content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
409
+ content += mostRecentContent + '\n';
410
+ content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
411
+ }
412
+ } else {
413
+ content += `${C.dim}No research notes${C.reset}\n`;
192
414
  }
193
- } else {
194
- console.log(`${C.dim}No session-state.json found${C.reset}`);
195
- }
196
415
 
197
- // 4. DOCS STRUCTURE
198
- section('Documentation');
199
- const docsDir = 'docs';
200
- const docFolders = safeLs(docsDir).filter(f => {
201
- try {
202
- return fs.statSync(path.join(docsDir, f)).isDirectory();
203
- } catch {
204
- return false;
416
+ // 6. BUS MESSAGES
417
+ content += `\n${C.cyan}${C.bold}═══ Recent Agent Messages ═══${C.reset}\n`;
418
+ const busPath = 'docs/09-agents/bus/log.jsonl';
419
+ const busContent = safeRead(busPath);
420
+ if (busContent) {
421
+ const lines = busContent.trim().split('\n').filter(Boolean);
422
+ const recent = lines.slice(-5);
423
+ if (recent.length > 0) {
424
+ recent.forEach(line => {
425
+ try {
426
+ const msg = JSON.parse(line);
427
+ const time = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString() : '?';
428
+ content += ` ${C.dim}[${time}]${C.reset} ${msg.from || '?'}: ${msg.type || msg.message || '?'}\n`;
429
+ } catch {
430
+ content += ` ${C.dim}${line.substring(0, 80)}...${C.reset}\n`;
431
+ }
432
+ });
433
+ } else {
434
+ content += `${C.dim}No messages${C.reset}\n`;
435
+ }
436
+ } else {
437
+ content += `${C.dim}No bus log found${C.reset}\n`;
205
438
  }
206
- });
207
439
 
208
- if (docFolders.length > 0) {
209
- docFolders.forEach(folder => {
210
- const folderPath = path.join(docsDir, folder);
211
- const files = safeLs(folderPath);
212
- const mdFiles = files.filter(f => f.endsWith('.md'));
213
- const jsonFiles = files.filter(f => f.endsWith('.json') || f.endsWith('.jsonl'));
440
+ // 7. KEY FILES - Full content
441
+ content += `\n${C.cyan}${C.bold}═══ Key Context Files (Full Content) ═══${C.reset}\n`;
214
442
 
215
- let info = [];
216
- if (mdFiles.length > 0) info.push(`${mdFiles.length} md`);
217
- if (jsonFiles.length > 0) info.push(`${jsonFiles.length} json`);
443
+ const keyFilesToRead = [
444
+ { path: 'CLAUDE.md', label: 'CLAUDE.md (Project Instructions)' },
445
+ { path: 'README.md', label: 'README.md (Project Overview)' },
446
+ { path: 'docs/04-architecture/README.md', label: 'Architecture Index' },
447
+ { path: 'docs/02-practices/README.md', label: 'Practices Index' },
448
+ { path: 'docs/08-project/roadmap.md', label: 'Roadmap' },
449
+ ];
218
450
 
219
- console.log(` ${C.dim}${folder}/${C.reset} ${info.length > 0 ? `(${info.join(', ')})` : ''}`);
451
+ keyFilesToRead.forEach(({ path: filePath, label }) => {
452
+ const fileContent = safeRead(filePath);
453
+ if (fileContent) {
454
+ content += `\n${C.green}✓ ${label}${C.reset} ${C.dim}(${filePath})${C.reset}\n`;
455
+ content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
456
+ content += fileContent + '\n';
457
+ content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
458
+ } else {
459
+ content += `${C.dim}○ ${label} (not found)${C.reset}\n`;
460
+ }
220
461
  });
221
- }
222
462
 
223
- // 5. RESEARCH NOTES
224
- section('Research Notes');
225
- const researchDir = 'docs/10-research';
226
- const researchFiles = safeLs(researchDir).filter(f => f.endsWith('.md') && f !== 'README.md');
227
- if (researchFiles.length > 0) {
228
- // Sort by date (filename starts with YYYYMMDD)
229
- researchFiles.sort().reverse();
230
- researchFiles.slice(0, 5).forEach(file => {
231
- console.log(` ${C.dim}${file}${C.reset}`);
232
- });
233
- if (researchFiles.length > 5) {
234
- console.log(` ${C.dim}... and ${researchFiles.length - 5} more${C.reset}`);
235
- }
236
- } else {
237
- console.log(`${C.dim}No research notes${C.reset}`);
238
- }
463
+ const settingsExists = fs.existsSync('.claude/settings.json');
464
+ content += `\n ${settingsExists ? `${C.green}✓${C.reset}` : `${C.dim}○${C.reset}`} .claude/settings.json\n`;
239
465
 
240
- // 6. BUS MESSAGES (last 5)
241
- section('Recent Agent Messages');
242
- const busPath = 'docs/09-agents/bus/log.jsonl';
243
- const busContent = safeRead(busPath);
244
- if (busContent) {
245
- const lines = busContent.trim().split('\n').filter(Boolean);
246
- const recent = lines.slice(-5);
247
- if (recent.length > 0) {
248
- recent.forEach(line => {
249
- try {
250
- const msg = JSON.parse(line);
251
- const time = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString() : '?';
252
- console.log(` ${C.dim}[${time}]${C.reset} ${msg.from || '?'}: ${msg.type || msg.message || '?'}`);
253
- } catch {
254
- console.log(` ${C.dim}${line.substring(0, 80)}...${C.reset}`);
255
- }
256
- });
466
+ // 8. EPICS FOLDER
467
+ content += `\n${C.cyan}${C.bold}═══ Epic Files ═══${C.reset}\n`;
468
+ const epicFiles = safeLs('docs/05-epics').filter(f => f.endsWith('.md') && f !== 'README.md');
469
+ if (epicFiles.length > 0) {
470
+ epicFiles.forEach(file => (content += ` ${C.dim}${file}${C.reset}\n`));
257
471
  } else {
258
- console.log(`${C.dim}No messages${C.reset}`);
472
+ content += `${C.dim}No epic files${C.reset}\n`;
259
473
  }
260
- } else {
261
- console.log(`${C.dim}No bus log found${C.reset}`);
474
+
475
+ // FOOTER
476
+ content += `\n${C.dim}─────────────────────────────────────────${C.reset}\n`;
477
+ content += `${C.dim}Context gathered in single execution. Claude has full context.${C.reset}\n`;
478
+
479
+ return content;
262
480
  }
263
481
 
264
- // 7. KEY FILES PRESENCE
265
- section('Key Files');
266
- const keyFiles = [
267
- { path: 'CLAUDE.md', label: 'CLAUDE.md (project instructions)' },
268
- { path: 'README.md', label: 'README.md (project overview)' },
269
- { path: 'docs/08-project/roadmap.md', label: 'Roadmap' },
270
- { path: 'docs/02-practices/README.md', label: 'Practices index' },
271
- { path: '.claude/settings.json', label: 'Claude settings' },
272
- ];
273
-
274
- keyFiles.forEach(({ path: filePath, label }) => {
275
- const exists = fs.existsSync(filePath);
276
- const icon = exists ? `${C.green}✓${C.reset}` : `${C.dim}○${C.reset}`;
277
- console.log(` ${icon} ${label}`);
278
- });
279
-
280
- // 8. EPICS FOLDER
281
- section('Epic Files');
282
- const epicFiles = safeLs('docs/05-epics').filter(f => f.endsWith('.md') && f !== 'README.md');
283
- if (epicFiles.length > 0) {
284
- epicFiles.forEach(file => {
285
- console.log(` ${C.dim}${file}${C.reset}`);
286
- });
482
+ // ============================================
483
+ // MAIN: Output with smart summary positioning
484
+ // ============================================
485
+
486
+ const summary = generateSummary();
487
+ const fullContent = generateFullContent();
488
+
489
+ const summaryLength = summary.length;
490
+ const cutoffPoint = DISPLAY_LIMIT - summaryLength;
491
+
492
+ if (fullContent.length <= cutoffPoint) {
493
+ // Full content fits before summary - just output everything
494
+ console.log(fullContent);
495
+ console.log(summary);
287
496
  } else {
288
- console.log(`${C.dim}No epic files${C.reset}`);
289
- }
497
+ // Split: content before cutoff + summary + content after cutoff
498
+ const contentBefore = fullContent.substring(0, cutoffPoint);
499
+ const contentAfter = fullContent.substring(cutoffPoint);
290
500
 
291
- // FOOTER
292
- console.log(`\n${C.dim}─────────────────────────────────────────${C.reset}`);
293
- console.log(`${C.dim}Context gathered in single execution. Ready for task selection.${C.reset}\n`);
501
+ console.log(contentBefore);
502
+ console.log(summary);
503
+ console.log(contentAfter);
504
+ }