agileflow 2.91.0 → 2.92.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/CHANGELOG.md +5 -0
- package/README.md +3 -3
- package/lib/README.md +178 -0
- package/lib/codebase-indexer.js +31 -23
- package/lib/colors.js +190 -12
- package/lib/consent.js +232 -0
- package/lib/correlation.js +277 -0
- package/lib/error-codes.js +46 -0
- package/lib/errors.js +48 -6
- package/lib/file-cache.js +182 -0
- package/lib/format-error.js +156 -0
- package/lib/path-resolver.js +155 -7
- package/lib/paths.js +212 -20
- package/lib/placeholder-registry.js +205 -0
- package/lib/registry-di.js +358 -0
- package/lib/result-schema.js +363 -0
- package/lib/result.js +210 -0
- package/lib/session-registry.js +13 -0
- package/lib/session-state-machine.js +465 -0
- package/lib/validate-commands.js +308 -0
- package/lib/validate.js +116 -52
- package/package.json +1 -1
- package/scripts/af +34 -0
- package/scripts/agent-loop.js +63 -9
- package/scripts/agileflow-configure.js +2 -2
- package/scripts/agileflow-welcome.js +435 -23
- package/scripts/archive-completed-stories.sh +57 -11
- package/scripts/claude-tmux.sh +102 -0
- package/scripts/damage-control-bash.js +3 -70
- package/scripts/damage-control-edit.js +3 -20
- package/scripts/damage-control-write.js +3 -20
- package/scripts/dependency-check.js +310 -0
- package/scripts/get-env.js +11 -4
- package/scripts/lib/configure-detect.js +23 -1
- package/scripts/lib/configure-features.js +43 -2
- package/scripts/lib/context-formatter.js +771 -0
- package/scripts/lib/context-loader.js +699 -0
- package/scripts/lib/damage-control-utils.js +107 -0
- package/scripts/lib/json-utils.sh +162 -0
- package/scripts/lib/state-migrator.js +353 -0
- package/scripts/lib/story-state-machine.js +437 -0
- package/scripts/obtain-context.js +80 -1248
- package/scripts/pre-push-check.sh +46 -0
- package/scripts/precompact-context.sh +23 -10
- package/scripts/query-codebase.js +122 -14
- package/scripts/ralph-loop.js +5 -5
- package/scripts/session-manager.js +220 -42
- package/scripts/spawn-parallel.js +651 -0
- package/scripts/tui/blessed/data/watcher.js +20 -15
- package/scripts/tui/blessed/index.js +2 -2
- package/scripts/tui/blessed/panels/output.js +14 -8
- package/scripts/tui/blessed/panels/sessions.js +22 -15
- package/scripts/tui/blessed/panels/trace.js +14 -8
- package/scripts/tui/blessed/ui/help.js +3 -3
- package/scripts/tui/blessed/ui/screen.js +4 -4
- package/scripts/tui/blessed/ui/statusbar.js +5 -9
- package/scripts/tui/blessed/ui/tabbar.js +11 -11
- package/scripts/validators/component-validator.js +41 -14
- package/scripts/validators/json-schema-validator.js +11 -4
- package/scripts/validators/markdown-validator.js +1 -2
- package/scripts/validators/migration-validator.js +17 -5
- package/scripts/validators/security-validator.js +137 -33
- package/scripts/validators/story-format-validator.js +31 -10
- package/scripts/validators/test-result-validator.js +19 -4
- package/scripts/validators/workflow-validator.js +12 -5
- package/src/core/agents/codebase-query.md +24 -0
- package/src/core/commands/adr.md +114 -0
- package/src/core/commands/agent.md +120 -0
- package/src/core/commands/assign.md +145 -0
- package/src/core/commands/babysit.md +32 -5
- package/src/core/commands/changelog.md +118 -0
- package/src/core/commands/configure.md +42 -6
- package/src/core/commands/diagnose.md +114 -0
- package/src/core/commands/epic.md +113 -0
- package/src/core/commands/handoff.md +128 -0
- package/src/core/commands/help.md +75 -0
- package/src/core/commands/pr.md +96 -0
- package/src/core/commands/roadmap/analyze.md +400 -0
- package/src/core/commands/session/new.md +113 -6
- package/src/core/commands/session/spawn.md +197 -0
- package/src/core/commands/sprint.md +22 -0
- package/src/core/commands/status.md +74 -0
- package/src/core/commands/story.md +143 -4
- package/src/core/templates/agileflow-metadata.json +55 -2
- package/src/core/templates/plan-template.md +125 -0
- package/src/core/templates/story-lifecycle.md +213 -0
- package/src/core/templates/story-template.md +4 -0
- package/src/core/templates/tdd-test-template.js +241 -0
- package/tools/cli/commands/setup.js +86 -0
- package/tools/cli/installers/core/installer.js +94 -0
- package/tools/cli/installers/ide/_base-ide.js +20 -11
- package/tools/cli/installers/ide/codex.js +29 -47
- package/tools/cli/lib/config-manager.js +17 -2
- package/tools/cli/lib/content-transformer.js +271 -0
- package/tools/cli/lib/error-handler.js +14 -22
- package/tools/cli/lib/ide-error-factory.js +421 -0
- package/tools/cli/lib/ide-health-monitor.js +364 -0
- package/tools/cli/lib/ide-registry.js +114 -1
- package/tools/cli/lib/ui.js +14 -25
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* context-formatter.js
|
|
4
|
+
*
|
|
5
|
+
* Output formatting module for obtain-context.js (US-0148)
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* - Summary table generation (compact overview)
|
|
9
|
+
* - Full content generation (detailed context)
|
|
10
|
+
* - Context budget warning display
|
|
11
|
+
* - String padding and truncation utilities
|
|
12
|
+
* - Box drawing and color formatting
|
|
13
|
+
*
|
|
14
|
+
* Note: Uses pre-fetched data from context-loader.js for all formatting
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
// Import colors and helpers
|
|
21
|
+
let C, box;
|
|
22
|
+
try {
|
|
23
|
+
const colors = require('../../lib/colors');
|
|
24
|
+
C = colors.c;
|
|
25
|
+
box = colors.box;
|
|
26
|
+
} catch {
|
|
27
|
+
// Fallback if colors not available
|
|
28
|
+
C = {
|
|
29
|
+
dim: '',
|
|
30
|
+
reset: '',
|
|
31
|
+
bold: '',
|
|
32
|
+
brand: '',
|
|
33
|
+
coral: '',
|
|
34
|
+
amber: '',
|
|
35
|
+
skyBlue: '',
|
|
36
|
+
mintGreen: '',
|
|
37
|
+
peach: '',
|
|
38
|
+
lavender: '',
|
|
39
|
+
teal: '',
|
|
40
|
+
cyan: '',
|
|
41
|
+
blue: '',
|
|
42
|
+
green: '',
|
|
43
|
+
lightGreen: '',
|
|
44
|
+
lightYellow: '',
|
|
45
|
+
};
|
|
46
|
+
box = {};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Import loader for fallback sync reads
|
|
50
|
+
const {
|
|
51
|
+
safeRead,
|
|
52
|
+
safeReadJSON,
|
|
53
|
+
safeLs,
|
|
54
|
+
safeExec,
|
|
55
|
+
getContextPercentage,
|
|
56
|
+
} = require('./context-loader');
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// String Utilities
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Pad string to length, accounting for ANSI escape codes.
|
|
64
|
+
* @param {string} str - String to pad
|
|
65
|
+
* @param {number} len - Target length
|
|
66
|
+
* @returns {string} Padded string
|
|
67
|
+
*/
|
|
68
|
+
function pad(str, len) {
|
|
69
|
+
const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
70
|
+
const diff = len - stripped.length;
|
|
71
|
+
if (diff <= 0) return str;
|
|
72
|
+
return str + ' '.repeat(diff);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Truncate string to max length, respecting ANSI codes.
|
|
77
|
+
* @param {string} str - String to truncate
|
|
78
|
+
* @param {number} maxLen - Maximum length
|
|
79
|
+
* @param {string} suffix - Suffix for truncated strings
|
|
80
|
+
* @returns {string} Truncated string
|
|
81
|
+
*/
|
|
82
|
+
function truncate(str, maxLen, suffix = '..') {
|
|
83
|
+
const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
84
|
+
if (stripped.length <= maxLen) return str;
|
|
85
|
+
|
|
86
|
+
const targetLen = maxLen - suffix.length;
|
|
87
|
+
let visibleCount = 0;
|
|
88
|
+
let cutIndex = 0;
|
|
89
|
+
let inEscape = false;
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < str.length; i++) {
|
|
92
|
+
if (str[i] === '\x1b') {
|
|
93
|
+
inEscape = true;
|
|
94
|
+
} else if (inEscape && str[i] === 'm') {
|
|
95
|
+
inEscape = false;
|
|
96
|
+
} else if (!inEscape) {
|
|
97
|
+
visibleCount++;
|
|
98
|
+
if (visibleCount >= targetLen) {
|
|
99
|
+
cutIndex = i + 1;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return str.substring(0, cutIndex) + suffix;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// Context Budget Warning (GSD Integration)
|
|
109
|
+
// =============================================================================
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Generate context budget warning box if usage exceeds threshold.
|
|
113
|
+
* Based on GSD research: 50% is where quality starts degrading.
|
|
114
|
+
*
|
|
115
|
+
* @param {number} percent - Context usage percentage
|
|
116
|
+
* @returns {string} Warning box or empty string
|
|
117
|
+
*/
|
|
118
|
+
function generateContextWarning(percent) {
|
|
119
|
+
if (percent < 50) {
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const width = 60;
|
|
124
|
+
const topLine = `┏${'━'.repeat(width - 2)}┓`;
|
|
125
|
+
const bottomLine = `┗${'━'.repeat(width - 2)}┛`;
|
|
126
|
+
|
|
127
|
+
let color, icon, message, suggestion;
|
|
128
|
+
|
|
129
|
+
if (percent >= 70) {
|
|
130
|
+
color = C.coral;
|
|
131
|
+
icon = '🔴';
|
|
132
|
+
message = `Context usage: ${percent}% (in degradation zone)`;
|
|
133
|
+
suggestion = 'Strongly recommend: compact conversation or delegate to sub-agent';
|
|
134
|
+
} else {
|
|
135
|
+
color = C.amber;
|
|
136
|
+
icon = '⚠️';
|
|
137
|
+
message = `Context usage: ${percent}% (approaching 50% threshold)`;
|
|
138
|
+
suggestion = 'Consider: delegate to sub-agent or compact conversation';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const msgPadded = ` ${icon} ${message}`.padEnd(width - 3) + '┃';
|
|
142
|
+
const sugPadded = ` → ${suggestion}`.padEnd(width - 3) + '┃';
|
|
143
|
+
|
|
144
|
+
return [
|
|
145
|
+
`${color}${C.bold}${topLine}${C.reset}`,
|
|
146
|
+
`${color}${C.bold}┃${msgPadded}${C.reset}`,
|
|
147
|
+
`${color}${C.bold}┃${sugPadded}${C.reset}`,
|
|
148
|
+
`${color}${C.bold}${bottomLine}${C.reset}`,
|
|
149
|
+
'',
|
|
150
|
+
].join('\n');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// =============================================================================
|
|
154
|
+
// Summary Generation
|
|
155
|
+
// =============================================================================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Generate summary content using pre-fetched data.
|
|
159
|
+
*
|
|
160
|
+
* @param {Object} prefetched - Pre-fetched data from prefetchAllData()
|
|
161
|
+
* @param {Object} options - Additional options
|
|
162
|
+
* @param {string} options.commandName - Active command name
|
|
163
|
+
* @param {string[]} options.activeSections - Active progressive disclosure sections
|
|
164
|
+
* @returns {string} Summary content
|
|
165
|
+
*/
|
|
166
|
+
function generateSummary(prefetched = null, options = {}) {
|
|
167
|
+
const { commandName = null, activeSections = [] } = options;
|
|
168
|
+
|
|
169
|
+
// Box drawing characters
|
|
170
|
+
const boxChars = {
|
|
171
|
+
tl: '╭',
|
|
172
|
+
tr: '╮',
|
|
173
|
+
bl: '╰',
|
|
174
|
+
br: '╯',
|
|
175
|
+
h: '─',
|
|
176
|
+
v: '│',
|
|
177
|
+
lT: '├',
|
|
178
|
+
rT: '┤',
|
|
179
|
+
tT: '┬',
|
|
180
|
+
bT: '┴',
|
|
181
|
+
cross: '┼',
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const W = 58; // Total inner width
|
|
185
|
+
const L = 20; // Left column width
|
|
186
|
+
const R = W - 24; // Right column width (34 chars)
|
|
187
|
+
|
|
188
|
+
// Create a row with auto-truncation
|
|
189
|
+
function row(left, right, leftColor = '', rightColor = '') {
|
|
190
|
+
const leftStr = `${leftColor}${left}${leftColor ? C.reset : ''}`;
|
|
191
|
+
const rightTrunc = truncate(right, R);
|
|
192
|
+
const rightStr = `${rightColor}${rightTrunc}${rightColor ? C.reset : ''}`;
|
|
193
|
+
return `${C.dim}${boxChars.v}${C.reset} ${pad(leftStr, L)} ${C.dim}${boxChars.v}${C.reset} ${pad(rightStr, R)} ${C.dim}${boxChars.v}${C.reset}\n`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const divider = () =>
|
|
197
|
+
`${C.dim}${boxChars.lT}${boxChars.h.repeat(L + 2)}${boxChars.cross}${boxChars.h.repeat(W - L - 2)}${boxChars.rT}${C.reset}\n`;
|
|
198
|
+
const headerTopBorder = `${C.dim}${boxChars.tl}${boxChars.h.repeat(L + 2)}${boxChars.tT}${boxChars.h.repeat(W - L - 2)}${boxChars.tr}${C.reset}\n`;
|
|
199
|
+
const headerDivider = `${C.dim}${boxChars.lT}${boxChars.h.repeat(L + 2)}${boxChars.tT}${boxChars.h.repeat(W - L - 2)}${boxChars.rT}${C.reset}\n`;
|
|
200
|
+
const bottomBorder = `${C.dim}${boxChars.bl}${boxChars.h.repeat(L + 2)}${boxChars.bT}${boxChars.h.repeat(W - L - 2)}${boxChars.br}${C.reset}\n`;
|
|
201
|
+
|
|
202
|
+
// Gather data - use prefetched when available, fallback to sync reads
|
|
203
|
+
const branch = prefetched?.git?.branch ?? safeExec('git branch --show-current') ?? 'unknown';
|
|
204
|
+
const lastCommitShort =
|
|
205
|
+
prefetched?.git?.commitShort ?? safeExec('git log -1 --format="%h"') ?? '?';
|
|
206
|
+
const lastCommitMsg =
|
|
207
|
+
prefetched?.git?.commitMsg ?? safeExec('git log -1 --format="%s"') ?? 'no commits';
|
|
208
|
+
const statusLines = (prefetched?.git?.status ?? safeExec('git status --short') ?? '')
|
|
209
|
+
.split('\n')
|
|
210
|
+
.filter(Boolean);
|
|
211
|
+
const statusJson = prefetched?.json?.statusJson ?? safeReadJSON('docs/09-agents/status.json');
|
|
212
|
+
const sessionState =
|
|
213
|
+
prefetched?.json?.sessionState ?? safeReadJSON('docs/09-agents/session-state.json');
|
|
214
|
+
const researchFiles =
|
|
215
|
+
prefetched?.researchFiles ??
|
|
216
|
+
safeLs('docs/10-research')
|
|
217
|
+
.filter(f => f.endsWith('.md') && f !== 'README.md')
|
|
218
|
+
.sort()
|
|
219
|
+
.reverse();
|
|
220
|
+
const epicFiles =
|
|
221
|
+
prefetched?.dirs?.epics?.filter(f => f.endsWith('.md') && f !== 'README.md') ??
|
|
222
|
+
safeLs('docs/05-epics').filter(f => f.endsWith('.md') && f !== 'README.md');
|
|
223
|
+
|
|
224
|
+
// Count stories by status
|
|
225
|
+
const byStatus = {};
|
|
226
|
+
const readyStories = [];
|
|
227
|
+
if (statusJson && statusJson.stories) {
|
|
228
|
+
Object.entries(statusJson.stories).forEach(([id, story]) => {
|
|
229
|
+
const s = story.status || 'unknown';
|
|
230
|
+
byStatus[s] = (byStatus[s] || 0) + 1;
|
|
231
|
+
if (s === 'ready') readyStories.push(id);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Session info
|
|
236
|
+
let sessionDuration = null;
|
|
237
|
+
let currentStory = null;
|
|
238
|
+
if (sessionState && sessionState.current_session && sessionState.current_session.started_at) {
|
|
239
|
+
const started = new Date(sessionState.current_session.started_at);
|
|
240
|
+
sessionDuration = Math.round((Date.now() - started.getTime()) / 60000);
|
|
241
|
+
currentStory = sessionState.current_session.current_story;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Build table
|
|
245
|
+
let summary = '\n';
|
|
246
|
+
summary += headerTopBorder;
|
|
247
|
+
|
|
248
|
+
// Header row
|
|
249
|
+
const title = commandName ? `Context [${commandName}]` : 'Context Summary';
|
|
250
|
+
const branchColor =
|
|
251
|
+
branch === 'main' ? C.mintGreen : branch.startsWith('fix') ? C.coral : C.skyBlue;
|
|
252
|
+
const maxBranchLen = 20;
|
|
253
|
+
const branchDisplay =
|
|
254
|
+
branch.length > maxBranchLen ? branch.substring(0, maxBranchLen - 2) + '..' : branch;
|
|
255
|
+
const header = `${C.brand}${C.bold}${title}${C.reset} ${branchColor}${branchDisplay}${C.reset} ${C.dim}(${lastCommitShort})${C.reset}`;
|
|
256
|
+
summary += `${C.dim}${boxChars.v}${C.reset} ${pad(header, W - 1)} ${C.dim}${boxChars.v}${C.reset}\n`;
|
|
257
|
+
|
|
258
|
+
summary += headerDivider;
|
|
259
|
+
|
|
260
|
+
// Story counts
|
|
261
|
+
summary += row(
|
|
262
|
+
'In Progress',
|
|
263
|
+
byStatus['in-progress'] ? `${byStatus['in-progress']}` : '0',
|
|
264
|
+
C.peach,
|
|
265
|
+
byStatus['in-progress'] ? C.peach : C.dim
|
|
266
|
+
);
|
|
267
|
+
summary += row(
|
|
268
|
+
'Blocked',
|
|
269
|
+
byStatus['blocked'] ? `${byStatus['blocked']}` : '0',
|
|
270
|
+
C.coral,
|
|
271
|
+
byStatus['blocked'] ? C.coral : C.dim
|
|
272
|
+
);
|
|
273
|
+
summary += row(
|
|
274
|
+
'Ready',
|
|
275
|
+
byStatus['ready'] ? `${byStatus['ready']}` : '0',
|
|
276
|
+
C.skyBlue,
|
|
277
|
+
byStatus['ready'] ? C.skyBlue : C.dim
|
|
278
|
+
);
|
|
279
|
+
const completedColor = `${C.bold}${C.mintGreen}`;
|
|
280
|
+
summary += row(
|
|
281
|
+
'Completed',
|
|
282
|
+
byStatus['done'] ? `${byStatus['done']}` : '0',
|
|
283
|
+
completedColor,
|
|
284
|
+
byStatus['done'] ? completedColor : C.dim
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
summary += divider();
|
|
288
|
+
|
|
289
|
+
// Git status
|
|
290
|
+
const uncommittedStatus =
|
|
291
|
+
statusLines.length > 0 ? `${statusLines.length} uncommitted` : '✓ clean';
|
|
292
|
+
summary += row('Git', uncommittedStatus, C.blue, statusLines.length > 0 ? C.peach : C.mintGreen);
|
|
293
|
+
|
|
294
|
+
// Session
|
|
295
|
+
const sessionText = sessionDuration !== null ? `${sessionDuration} min active` : 'no session';
|
|
296
|
+
summary += row('Session', sessionText, C.blue, sessionDuration !== null ? C.lightGreen : C.dim);
|
|
297
|
+
|
|
298
|
+
// Current story
|
|
299
|
+
const storyText = currentStory ? currentStory : 'none';
|
|
300
|
+
summary += row('Working on', storyText, C.blue, currentStory ? C.lightYellow : C.dim);
|
|
301
|
+
|
|
302
|
+
// Ready stories
|
|
303
|
+
if (readyStories.length > 0) {
|
|
304
|
+
summary += row('⭐ Up Next', readyStories.slice(0, 3).join(', '), C.skyBlue, C.skyBlue);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Progressive disclosure: Show active sections
|
|
308
|
+
if (activeSections.length > 0) {
|
|
309
|
+
summary += divider();
|
|
310
|
+
const sectionList = activeSections.join(', ');
|
|
311
|
+
summary += row('📖 Sections', sectionList, C.cyan, C.mintGreen);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
summary += divider();
|
|
315
|
+
|
|
316
|
+
// Key files
|
|
317
|
+
const keyFileChecks = [
|
|
318
|
+
{ path: 'CLAUDE.md', label: 'CLAUDE' },
|
|
319
|
+
{ path: 'README.md', label: 'README' },
|
|
320
|
+
{ path: 'docs/04-architecture/README.md', label: 'arch' },
|
|
321
|
+
{ path: 'docs/02-practices/README.md', label: 'practices' },
|
|
322
|
+
];
|
|
323
|
+
const keyFileStatus = keyFileChecks
|
|
324
|
+
.map(f => {
|
|
325
|
+
const exists = fs.existsSync(f.path);
|
|
326
|
+
return exists ? `${C.mintGreen}✓${C.reset}${f.label}` : `${C.dim}○${f.label}${C.reset}`;
|
|
327
|
+
})
|
|
328
|
+
.join(' ');
|
|
329
|
+
summary += row('Key files', keyFileStatus, C.lavender, '');
|
|
330
|
+
|
|
331
|
+
// Research
|
|
332
|
+
const researchText = researchFiles.length > 0 ? `${researchFiles.length} notes` : 'none';
|
|
333
|
+
summary += row(
|
|
334
|
+
'Research',
|
|
335
|
+
researchText,
|
|
336
|
+
C.lavender,
|
|
337
|
+
researchFiles.length > 0 ? C.skyBlue : C.dim
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// Epics
|
|
341
|
+
const epicText = epicFiles.length > 0 ? `${epicFiles.length} epics` : 'none';
|
|
342
|
+
summary += row('Epics', epicText, C.lavender, epicFiles.length > 0 ? C.skyBlue : C.dim);
|
|
343
|
+
|
|
344
|
+
summary += divider();
|
|
345
|
+
|
|
346
|
+
// Last commit
|
|
347
|
+
summary += row(
|
|
348
|
+
'Last commit',
|
|
349
|
+
`${C.peach}${lastCommitShort}${C.reset} ${lastCommitMsg}`,
|
|
350
|
+
C.dim,
|
|
351
|
+
''
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
summary += bottomBorder;
|
|
355
|
+
|
|
356
|
+
return summary;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// =============================================================================
|
|
360
|
+
// Full Content Generation
|
|
361
|
+
// =============================================================================
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Generate full content using pre-fetched data.
|
|
365
|
+
*
|
|
366
|
+
* @param {Object} prefetched - Pre-fetched data from prefetchAllData()
|
|
367
|
+
* @param {Object} options - Additional options
|
|
368
|
+
* @param {string} options.commandName - Active command name
|
|
369
|
+
* @param {string[]} options.activeSections - Active progressive disclosure sections
|
|
370
|
+
* @returns {string} Full content
|
|
371
|
+
*/
|
|
372
|
+
function generateFullContent(prefetched = null, options = {}) {
|
|
373
|
+
const { commandName = null, activeSections = [] } = options;
|
|
374
|
+
|
|
375
|
+
let content = '';
|
|
376
|
+
|
|
377
|
+
const title = commandName ? `AgileFlow Context [${commandName}]` : 'AgileFlow Context';
|
|
378
|
+
content += `${C.lavender}${C.bold}${title}${C.reset}\n`;
|
|
379
|
+
content += `${C.dim}Generated: ${new Date().toISOString()}${C.reset}\n`;
|
|
380
|
+
|
|
381
|
+
// SESSION CONTEXT BANNER
|
|
382
|
+
const sessionManagerPath = path.join(__dirname, '..', 'session-manager.js');
|
|
383
|
+
const altSessionManagerPath = '.agileflow/scripts/session-manager.js';
|
|
384
|
+
|
|
385
|
+
if (fs.existsSync(sessionManagerPath) || fs.existsSync(altSessionManagerPath)) {
|
|
386
|
+
const managerPath = fs.existsSync(sessionManagerPath)
|
|
387
|
+
? sessionManagerPath
|
|
388
|
+
: altSessionManagerPath;
|
|
389
|
+
const sessionStatus = safeExec(`node "${managerPath}" status`);
|
|
390
|
+
|
|
391
|
+
if (sessionStatus) {
|
|
392
|
+
try {
|
|
393
|
+
const statusData = JSON.parse(sessionStatus);
|
|
394
|
+
if (statusData.current) {
|
|
395
|
+
const session = statusData.current;
|
|
396
|
+
const isMain = session.is_main === true;
|
|
397
|
+
const sessionName = session.nickname
|
|
398
|
+
? `Session ${session.id} "${session.nickname}"`
|
|
399
|
+
: `Session ${session.id}`;
|
|
400
|
+
|
|
401
|
+
content += `\n${C.teal}${C.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n`;
|
|
402
|
+
content += `${C.teal}${C.bold}📍 SESSION CONTEXT${C.reset}\n`;
|
|
403
|
+
content += `${C.teal}${C.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n`;
|
|
404
|
+
|
|
405
|
+
if (isMain) {
|
|
406
|
+
content += `${C.mintGreen}${C.bold}${sessionName}${C.reset} ${C.dim}(main project)${C.reset}\n`;
|
|
407
|
+
} else {
|
|
408
|
+
content += `${C.peach}${C.bold}🔀 ${sessionName}${C.reset} ${C.dim}(worktree)${C.reset}\n`;
|
|
409
|
+
content += `Branch: ${C.skyBlue}${session.branch || 'unknown'}${C.reset}\n`;
|
|
410
|
+
content += `${C.dim}Path: ${session.path || process.cwd()}${C.reset}\n`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (statusData.otherActive > 0) {
|
|
414
|
+
content += `${C.amber}⚠️ ${statusData.otherActive} other active session(s)${C.reset} - check story claims below\n`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
content += `${C.teal}${C.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n\n`;
|
|
418
|
+
}
|
|
419
|
+
} catch {
|
|
420
|
+
// Silently ignore session parse errors
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// INTERACTION MODE (AskUserQuestion)
|
|
426
|
+
const earlyMetadata =
|
|
427
|
+
prefetched?.json?.metadata ?? safeReadJSON('docs/00-meta/agileflow-metadata.json');
|
|
428
|
+
const askUserQuestionConfig = earlyMetadata?.features?.askUserQuestion;
|
|
429
|
+
|
|
430
|
+
if (askUserQuestionConfig?.enabled) {
|
|
431
|
+
content += `${C.coral}${C.bold}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓${C.reset}\n`;
|
|
432
|
+
content += `${C.coral}${C.bold}┃ 🔔 MANDATORY: AskUserQuestion After EVERY Response ┃${C.reset}\n`;
|
|
433
|
+
content += `${C.coral}${C.bold}┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛${C.reset}\n`;
|
|
434
|
+
content += `${C.bold}After completing ANY task${C.reset} (implementation, fix, etc.):\n`;
|
|
435
|
+
content += `${C.mintGreen}→ ALWAYS${C.reset} call ${C.skyBlue}AskUserQuestion${C.reset} tool to offer next steps\n`;
|
|
436
|
+
content += `${C.coral}→ NEVER${C.reset} end with text like "Done!" or "What's next?"\n\n`;
|
|
437
|
+
content += `${C.dim}Balance: Use at natural pause points. Don't ask permission for routine work.${C.reset}\n\n`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// CONTEXT BUDGET WARNING
|
|
441
|
+
const contextUsage = getContextPercentage();
|
|
442
|
+
if (contextUsage && contextUsage.percent >= 50) {
|
|
443
|
+
content += generateContextWarning(contextUsage.percent);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// PROGRESSIVE DISCLOSURE
|
|
447
|
+
if (activeSections.length > 0) {
|
|
448
|
+
content += `\n${C.cyan}${C.bold}═══ 📖 Progressive Disclosure: Active Sections ═══${C.reset}\n`;
|
|
449
|
+
content += `${C.dim}The following sections are activated based on command parameters.${C.reset}\n`;
|
|
450
|
+
content += `${C.dim}Look for <!-- SECTION: name --> markers in the command file.${C.reset}\n\n`;
|
|
451
|
+
|
|
452
|
+
activeSections.forEach(section => {
|
|
453
|
+
content += ` ${C.mintGreen}✓${C.reset} ${C.bold}${section}${C.reset}\n`;
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const sectionDescriptions = {
|
|
457
|
+
'loop-mode': 'Autonomous epic execution (MODE=loop)',
|
|
458
|
+
'multi-session': 'Multi-session coordination detected',
|
|
459
|
+
'visual-e2e': 'Visual screenshot verification (VISUAL=true)',
|
|
460
|
+
delegation: 'Expert spawning patterns (load when spawning)',
|
|
461
|
+
stuck: 'Research prompt guidance (load after 2 failures)',
|
|
462
|
+
'plan-mode': 'Planning workflow details (load when entering plan mode)',
|
|
463
|
+
tools: 'Tool usage guidance (load when needed)',
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
content += `\n${C.dim}Section meanings:${C.reset}\n`;
|
|
467
|
+
activeSections.forEach(section => {
|
|
468
|
+
const desc = sectionDescriptions[section] || 'Conditional content';
|
|
469
|
+
content += ` ${C.dim}• ${section}: ${desc}${C.reset}\n`;
|
|
470
|
+
});
|
|
471
|
+
content += '\n';
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// GIT STATUS
|
|
475
|
+
content += `\n${C.skyBlue}${C.bold}═══ Git Status ═══${C.reset}\n`;
|
|
476
|
+
const branch = prefetched?.git?.branch ?? safeExec('git branch --show-current') ?? 'unknown';
|
|
477
|
+
const status = prefetched?.git?.status ?? safeExec('git status --short') ?? '';
|
|
478
|
+
const statusLines = status.split('\n').filter(Boolean);
|
|
479
|
+
const lastCommit =
|
|
480
|
+
prefetched?.git?.commitFull ?? safeExec('git log -1 --format="%h %s"') ?? 'no commits';
|
|
481
|
+
|
|
482
|
+
content += `Branch: ${C.mintGreen}${branch}${C.reset}\n`;
|
|
483
|
+
content += `Last commit: ${C.dim}${lastCommit}${C.reset}\n`;
|
|
484
|
+
if (statusLines.length > 0) {
|
|
485
|
+
content += `Uncommitted: ${C.peach}${statusLines.length} file(s)${C.reset}\n`;
|
|
486
|
+
statusLines.slice(0, 10).forEach(line => (content += ` ${C.dim}${line}${C.reset}\n`));
|
|
487
|
+
if (statusLines.length > 10)
|
|
488
|
+
content += ` ${C.dim}... and ${statusLines.length - 10} more${C.reset}\n`;
|
|
489
|
+
} else {
|
|
490
|
+
content += `Uncommitted: ${C.mintGreen}clean${C.reset}\n`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// STATUS.JSON
|
|
494
|
+
content += `\n${C.skyBlue}${C.bold}═══ Status.json (Full Content) ═══${C.reset}\n`;
|
|
495
|
+
const statusJson = prefetched?.json?.statusJson ?? safeReadJSON('docs/09-agents/status.json');
|
|
496
|
+
|
|
497
|
+
if (statusJson) {
|
|
498
|
+
content += `${C.dim}${'─'.repeat(50)}${C.reset}\n`;
|
|
499
|
+
content +=
|
|
500
|
+
JSON.stringify(statusJson, null, 2)
|
|
501
|
+
.split('\n')
|
|
502
|
+
.map(l => ` ${l}`)
|
|
503
|
+
.join('\n') + '\n';
|
|
504
|
+
content += `${C.dim}${'─'.repeat(50)}${C.reset}\n`;
|
|
505
|
+
} else {
|
|
506
|
+
content += `${C.dim}No status.json found${C.reset}\n`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// SESSION STATE
|
|
510
|
+
content += `\n${C.skyBlue}${C.bold}═══ Session State ═══${C.reset}\n`;
|
|
511
|
+
const sessionState =
|
|
512
|
+
prefetched?.json?.sessionState ?? safeReadJSON('docs/09-agents/session-state.json');
|
|
513
|
+
if (sessionState) {
|
|
514
|
+
const current = sessionState.current_session;
|
|
515
|
+
if (current && current.started_at) {
|
|
516
|
+
const started = new Date(current.started_at);
|
|
517
|
+
const duration = Math.round((Date.now() - started.getTime()) / 60000);
|
|
518
|
+
content += `Active session: ${C.lightGreen}${duration} min${C.reset}\n`;
|
|
519
|
+
if (current.current_story) {
|
|
520
|
+
content += `Working on: ${C.lightYellow}${current.current_story}${C.reset}\n`;
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
content += `${C.dim}No active session${C.reset}\n`;
|
|
524
|
+
}
|
|
525
|
+
if (Array.isArray(sessionState.active_commands) && sessionState.active_commands.length > 0) {
|
|
526
|
+
const cmdNames = sessionState.active_commands.map(c => c.name).join(', ');
|
|
527
|
+
content += `Active commands: ${C.skyBlue}${cmdNames}${C.reset}\n`;
|
|
528
|
+
} else if (sessionState.active_command) {
|
|
529
|
+
content += `Active command: ${C.skyBlue}${sessionState.active_command.name}${C.reset}\n`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const batchLoop = sessionState.batch_loop;
|
|
533
|
+
if (batchLoop && batchLoop.enabled) {
|
|
534
|
+
content += `\n${C.skyBlue}${C.bold}── Batch Loop Active ──${C.reset}\n`;
|
|
535
|
+
content += `Pattern: ${C.cyan}${batchLoop.pattern}${C.reset}\n`;
|
|
536
|
+
content += `Action: ${C.cyan}${batchLoop.action}${C.reset}\n`;
|
|
537
|
+
content += `Current: ${C.lightYellow}${batchLoop.current_item || 'none'}${C.reset}\n`;
|
|
538
|
+
const summary = batchLoop.summary || {};
|
|
539
|
+
content += `Progress: ${C.lightGreen}${summary.completed || 0}${C.reset}/${summary.total || 0} `;
|
|
540
|
+
content += `(${C.lightYellow}${summary.in_progress || 0}${C.reset} in progress)\n`;
|
|
541
|
+
content += `Iteration: ${batchLoop.iteration || 0}/${batchLoop.max_iterations || 50}\n`;
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
content += `${C.dim}No session-state.json found${C.reset}\n`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Remaining content would continue here...
|
|
548
|
+
// For brevity, returning the core content
|
|
549
|
+
// The full implementation would include all remaining sections
|
|
550
|
+
|
|
551
|
+
// Add remaining sections
|
|
552
|
+
content += generateRemainingContent(prefetched, options);
|
|
553
|
+
|
|
554
|
+
return content;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Generate remaining content sections (internal helper).
|
|
559
|
+
* @param {Object} prefetched - Pre-fetched data
|
|
560
|
+
* @param {Object} options - Options
|
|
561
|
+
* @returns {string} Remaining content
|
|
562
|
+
*/
|
|
563
|
+
function generateRemainingContent(prefetched, options = {}) {
|
|
564
|
+
let content = '';
|
|
565
|
+
|
|
566
|
+
// STORY CLAIMS
|
|
567
|
+
const shouldLoadClaims = prefetched?.sectionsToLoad?.sessionClaims !== false;
|
|
568
|
+
if (shouldLoadClaims) {
|
|
569
|
+
const storyClaimingPath = path.join(__dirname, 'story-claiming.js');
|
|
570
|
+
const altStoryClaimingPath = '.agileflow/scripts/lib/story-claiming.js';
|
|
571
|
+
|
|
572
|
+
if (fs.existsSync(storyClaimingPath) || fs.existsSync(altStoryClaimingPath)) {
|
|
573
|
+
try {
|
|
574
|
+
const claimPath = fs.existsSync(storyClaimingPath)
|
|
575
|
+
? storyClaimingPath
|
|
576
|
+
: altStoryClaimingPath;
|
|
577
|
+
const storyClaiming = require(claimPath);
|
|
578
|
+
|
|
579
|
+
const othersResult = storyClaiming.getStoriesClaimedByOthers();
|
|
580
|
+
if (othersResult.ok && othersResult.stories && othersResult.stories.length > 0) {
|
|
581
|
+
content += `\n${C.amber}${C.bold}═══ 🔒 Claimed Stories ═══${C.reset}\n`;
|
|
582
|
+
content += `${C.dim}Stories locked by other sessions - pick a different one${C.reset}\n`;
|
|
583
|
+
othersResult.stories.forEach(story => {
|
|
584
|
+
const sessionDir = story.claimedBy?.path
|
|
585
|
+
? path.basename(story.claimedBy.path)
|
|
586
|
+
: 'unknown';
|
|
587
|
+
content += ` ${C.coral}🔒${C.reset} ${C.lavender}${story.id}${C.reset} "${story.title}" ${C.dim}→ Session ${story.claimedBy?.session_id || '?'} (${sessionDir})${C.reset}\n`;
|
|
588
|
+
});
|
|
589
|
+
content += '\n';
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const myResult = storyClaiming.getClaimedStoriesForSession();
|
|
593
|
+
if (myResult.ok && myResult.stories && myResult.stories.length > 0) {
|
|
594
|
+
content += `\n${C.mintGreen}${C.bold}═══ ✓ Your Claimed Stories ═══${C.reset}\n`;
|
|
595
|
+
myResult.stories.forEach(story => {
|
|
596
|
+
content += ` ${C.mintGreen}✓${C.reset} ${C.lavender}${story.id}${C.reset} "${story.title}"\n`;
|
|
597
|
+
});
|
|
598
|
+
content += '\n';
|
|
599
|
+
}
|
|
600
|
+
} catch {
|
|
601
|
+
// Story claiming not available
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// VISUAL E2E STATUS
|
|
607
|
+
const metadata =
|
|
608
|
+
prefetched?.json?.metadata ?? safeReadJSON('docs/00-meta/agileflow-metadata.json');
|
|
609
|
+
const visualE2eConfig = metadata?.features?.visual_e2e;
|
|
610
|
+
const playwrightExists =
|
|
611
|
+
fs.existsSync('playwright.config.ts') || fs.existsSync('playwright.config.js');
|
|
612
|
+
const screenshotsExists = fs.existsSync('screenshots');
|
|
613
|
+
const testsE2eExists = fs.existsSync('tests/e2e');
|
|
614
|
+
|
|
615
|
+
const visualE2eEnabled = visualE2eConfig?.enabled || (playwrightExists && screenshotsExists);
|
|
616
|
+
|
|
617
|
+
if (visualE2eEnabled) {
|
|
618
|
+
content += `\n${C.brand}${C.bold}═══ 📸 VISUAL E2E TESTING: ENABLED ═══${C.reset}\n`;
|
|
619
|
+
content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
|
|
620
|
+
content += `${C.mintGreen}✓ Playwright:${C.reset} ${playwrightExists ? 'configured' : 'not found'}\n`;
|
|
621
|
+
content += `${C.mintGreen}✓ Screenshots:${C.reset} ${screenshotsExists ? 'screenshots/' : 'not found'}\n`;
|
|
622
|
+
content += `${C.mintGreen}✓ E2E Tests:${C.reset} ${testsE2eExists ? 'tests/e2e/' : 'not found'}\n\n`;
|
|
623
|
+
content += `${C.bold}FOR UI WORK:${C.reset} Use ${C.skyBlue}VISUAL=true${C.reset} flag with babysit:\n`;
|
|
624
|
+
content += `${C.dim} /agileflow:babysit EPIC=EP-XXXX MODE=loop VISUAL=true${C.reset}\n\n`;
|
|
625
|
+
content += `${C.dim}${'─'.repeat(60)}${C.reset}\n\n`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// DOCS STRUCTURE
|
|
629
|
+
content += `\n${C.skyBlue}${C.bold}═══ Documentation ═══${C.reset}\n`;
|
|
630
|
+
const docsDir = 'docs';
|
|
631
|
+
const docFolders = (prefetched?.dirs?.docs ?? safeLs(docsDir)).filter(f => {
|
|
632
|
+
try {
|
|
633
|
+
return fs.statSync(path.join(docsDir, f)).isDirectory();
|
|
634
|
+
} catch {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
if (docFolders.length > 0) {
|
|
640
|
+
docFolders.forEach(folder => {
|
|
641
|
+
const folderPath = path.join(docsDir, folder);
|
|
642
|
+
const files = safeLs(folderPath);
|
|
643
|
+
const mdFiles = files.filter(f => f.endsWith('.md'));
|
|
644
|
+
const jsonFiles = files.filter(f => f.endsWith('.json') || f.endsWith('.jsonl'));
|
|
645
|
+
const info = [];
|
|
646
|
+
if (mdFiles.length > 0) info.push(`${mdFiles.length} md`);
|
|
647
|
+
if (jsonFiles.length > 0) info.push(`${jsonFiles.length} json`);
|
|
648
|
+
content += ` ${C.dim}${folder}/${C.reset} ${info.length > 0 ? `(${info.join(', ')})` : ''}\n`;
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// RESEARCH NOTES
|
|
653
|
+
const shouldLoadResearch = prefetched?.sectionsToLoad?.researchContent !== false;
|
|
654
|
+
content += `\n${C.skyBlue}${C.bold}═══ Research Notes ═══${C.reset}\n`;
|
|
655
|
+
const researchDir = 'docs/10-research';
|
|
656
|
+
const researchFiles =
|
|
657
|
+
prefetched?.researchFiles ??
|
|
658
|
+
safeLs(researchDir)
|
|
659
|
+
.filter(f => f.endsWith('.md') && f !== 'README.md')
|
|
660
|
+
.sort()
|
|
661
|
+
.reverse();
|
|
662
|
+
if (researchFiles.length > 0) {
|
|
663
|
+
content += `${C.dim}───${C.reset} Available Research Notes\n`;
|
|
664
|
+
researchFiles.forEach(file => (content += ` ${C.dim}${file}${C.reset}\n`));
|
|
665
|
+
|
|
666
|
+
const mostRecentFile = researchFiles[0];
|
|
667
|
+
const mostRecentPath = path.join(researchDir, mostRecentFile);
|
|
668
|
+
const mostRecentContent =
|
|
669
|
+
prefetched?.mostRecentResearch ?? (shouldLoadResearch ? safeRead(mostRecentPath) : null);
|
|
670
|
+
|
|
671
|
+
if (mostRecentContent) {
|
|
672
|
+
content += `\n${C.mintGreen}📄 Most Recent: ${mostRecentFile}${C.reset}\n`;
|
|
673
|
+
content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
|
|
674
|
+
content += mostRecentContent + '\n';
|
|
675
|
+
content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
|
|
676
|
+
} else if (!shouldLoadResearch) {
|
|
677
|
+
content += `\n${C.dim}📄 Content deferred (lazy loading). Use /agileflow:research to access.${C.reset}\n`;
|
|
678
|
+
}
|
|
679
|
+
} else {
|
|
680
|
+
content += `${C.dim}No research notes${C.reset}\n`;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// BUS MESSAGES
|
|
684
|
+
content += `\n${C.skyBlue}${C.bold}═══ Recent Agent Messages ═══${C.reset}\n`;
|
|
685
|
+
const busPath = 'docs/09-agents/bus/log.jsonl';
|
|
686
|
+
const busContent = prefetched?.text?.busLog ?? safeRead(busPath);
|
|
687
|
+
if (busContent) {
|
|
688
|
+
const lines = busContent.trim().split('\n').filter(Boolean);
|
|
689
|
+
const recent = lines.slice(-5);
|
|
690
|
+
if (recent.length > 0) {
|
|
691
|
+
recent.forEach(line => {
|
|
692
|
+
try {
|
|
693
|
+
const msg = JSON.parse(line);
|
|
694
|
+
const time = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString() : '?';
|
|
695
|
+
content += ` ${C.dim}[${time}]${C.reset} ${msg.from || '?'}: ${msg.type || msg.message || '?'}\n`;
|
|
696
|
+
} catch {
|
|
697
|
+
content += ` ${C.dim}${line.substring(0, 80)}...${C.reset}\n`;
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
} else {
|
|
701
|
+
content += `${C.dim}No messages${C.reset}\n`;
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
content += `${C.dim}No bus log found${C.reset}\n`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// KEY FILES
|
|
708
|
+
content += `\n${C.cyan}${C.bold}═══ Key Context Files (Full Content) ═══${C.reset}\n`;
|
|
709
|
+
|
|
710
|
+
const prefetchedKeyMap = {
|
|
711
|
+
'CLAUDE.md': 'claudeMd',
|
|
712
|
+
'README.md': 'readmeMd',
|
|
713
|
+
'docs/04-architecture/README.md': 'archReadme',
|
|
714
|
+
'docs/02-practices/README.md': 'practicesReadme',
|
|
715
|
+
'docs/08-project/roadmap.md': 'roadmap',
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
const keyFilesToRead = [
|
|
719
|
+
{ path: 'CLAUDE.md', label: 'CLAUDE.md (Project Instructions)' },
|
|
720
|
+
{ path: 'README.md', label: 'README.md (Project Overview)' },
|
|
721
|
+
{ path: 'docs/04-architecture/README.md', label: 'Architecture Index' },
|
|
722
|
+
{ path: 'docs/02-practices/README.md', label: 'Practices Index' },
|
|
723
|
+
{ path: 'docs/08-project/roadmap.md', label: 'Roadmap' },
|
|
724
|
+
];
|
|
725
|
+
|
|
726
|
+
keyFilesToRead.forEach(({ path: filePath, label }) => {
|
|
727
|
+
const prefetchKey = prefetchedKeyMap[filePath];
|
|
728
|
+
const fileContent = prefetched?.text?.[prefetchKey] ?? safeRead(filePath);
|
|
729
|
+
if (fileContent) {
|
|
730
|
+
content += `\n${C.green}✓ ${label}${C.reset} ${C.dim}(${filePath})${C.reset}\n`;
|
|
731
|
+
content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
|
|
732
|
+
content += fileContent + '\n';
|
|
733
|
+
content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
|
|
734
|
+
} else {
|
|
735
|
+
content += `${C.dim}○ ${label} (not found)${C.reset}\n`;
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const settingsExists = fs.existsSync('.claude/settings.json');
|
|
740
|
+
content += `\n ${settingsExists ? `${C.green}✓${C.reset}` : `${C.dim}○${C.reset}`} .claude/settings.json\n`;
|
|
741
|
+
|
|
742
|
+
// EPICS FOLDER
|
|
743
|
+
content += `\n${C.cyan}${C.bold}═══ Epic Files ═══${C.reset}\n`;
|
|
744
|
+
const epicFiles =
|
|
745
|
+
prefetched?.dirs?.epics?.filter(f => f.endsWith('.md') && f !== 'README.md') ??
|
|
746
|
+
safeLs('docs/05-epics').filter(f => f.endsWith('.md') && f !== 'README.md');
|
|
747
|
+
if (epicFiles.length > 0) {
|
|
748
|
+
epicFiles.forEach(file => (content += ` ${C.dim}${file}${C.reset}\n`));
|
|
749
|
+
} else {
|
|
750
|
+
content += `${C.dim}No epic files${C.reset}\n`;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// FOOTER
|
|
754
|
+
content += `\n${C.dim}─────────────────────────────────────────${C.reset}\n`;
|
|
755
|
+
content += `${C.dim}Context gathered in single execution. Claude has full context.${C.reset}\n`;
|
|
756
|
+
|
|
757
|
+
return content;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
module.exports = {
|
|
761
|
+
// String utilities
|
|
762
|
+
pad,
|
|
763
|
+
truncate,
|
|
764
|
+
|
|
765
|
+
// Warning generation
|
|
766
|
+
generateContextWarning,
|
|
767
|
+
|
|
768
|
+
// Main generators
|
|
769
|
+
generateSummary,
|
|
770
|
+
generateFullContent,
|
|
771
|
+
};
|