agileflow 2.43.0 → 2.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/scripts/README.md +267 -0
- package/scripts/agileflow-configure.js +927 -0
- package/scripts/agileflow-statusline.sh +355 -0
- package/scripts/agileflow-stop.sh +13 -0
- package/scripts/agileflow-welcome.js +427 -0
- package/scripts/archive-completed-stories.sh +162 -0
- package/scripts/clear-active-command.js +48 -0
- package/scripts/compress-status.sh +116 -0
- package/scripts/expertise-metrics.sh +264 -0
- package/scripts/generate-all.sh +77 -0
- package/scripts/generators/agent-registry.js +167 -0
- package/scripts/generators/command-registry.js +135 -0
- package/scripts/generators/index.js +87 -0
- package/scripts/generators/inject-babysit.js +167 -0
- package/scripts/generators/inject-help.js +109 -0
- package/scripts/generators/inject-readme.js +156 -0
- package/scripts/generators/skill-registry.js +144 -0
- package/scripts/get-env.js +209 -0
- package/scripts/obtain-context.js +293 -0
- package/scripts/precompact-context.sh +123 -0
- package/scripts/validate-expertise.sh +259 -0
- package/src/core/commands/context.md +141 -5
- package/tools/cli/installers/core/installer.js +97 -4
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Skill Registry Scanner
|
|
5
|
+
*
|
|
6
|
+
* Scans skills/ directory and extracts metadata from SKILL.md frontmatter.
|
|
7
|
+
* Returns structured skill registry for use in generators.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract YAML frontmatter from markdown file
|
|
15
|
+
* @param {string} filePath - Path to markdown file
|
|
16
|
+
* @returns {object} Frontmatter object
|
|
17
|
+
*/
|
|
18
|
+
function extractFrontmatter(filePath) {
|
|
19
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
20
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
21
|
+
|
|
22
|
+
if (!frontmatterMatch) {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const frontmatter = {};
|
|
27
|
+
const lines = frontmatterMatch[1].split('\n');
|
|
28
|
+
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
31
|
+
if (match) {
|
|
32
|
+
const [, key, value] = match;
|
|
33
|
+
// Remove quotes if present
|
|
34
|
+
frontmatter[key] = value.replace(/^["']|["']$/g, '');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return frontmatter;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Categorize skill based on its name/description
|
|
43
|
+
* @param {string} name - Skill name
|
|
44
|
+
* @param {string} description - Skill description
|
|
45
|
+
* @returns {string} Category name
|
|
46
|
+
*/
|
|
47
|
+
function categorizeSkill(name, description) {
|
|
48
|
+
const categories = {
|
|
49
|
+
'Story & Planning': ['story', 'epic', 'sprint', 'acceptance-criteria'],
|
|
50
|
+
'Code Generation': ['type-definitions', 'validation-schema', 'error-handler'],
|
|
51
|
+
'Testing': ['test-case'],
|
|
52
|
+
'Documentation': ['adr', 'api-documentation', 'changelog', 'pr-description'],
|
|
53
|
+
'Architecture': ['sql-schema', 'diagram'],
|
|
54
|
+
'Deployment': ['deployment-guide', 'migration-checklist']
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const lowerName = name.toLowerCase();
|
|
58
|
+
const lowerDesc = description.toLowerCase();
|
|
59
|
+
|
|
60
|
+
for (const [category, keywords] of Object.entries(categories)) {
|
|
61
|
+
if (keywords.some(kw => lowerName.includes(kw) || lowerDesc.includes(kw))) {
|
|
62
|
+
return category;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return 'Other';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Scan skills directory and build registry
|
|
71
|
+
* @param {string} skillsDir - Path to skills directory
|
|
72
|
+
* @returns {Array} Array of skill metadata objects
|
|
73
|
+
*/
|
|
74
|
+
function scanSkills(skillsDir) {
|
|
75
|
+
const skills = [];
|
|
76
|
+
|
|
77
|
+
// Each skill is in its own directory with a SKILL.md file
|
|
78
|
+
const skillDirs = fs.readdirSync(skillsDir);
|
|
79
|
+
|
|
80
|
+
for (const skillDir of skillDirs) {
|
|
81
|
+
const skillPath = path.join(skillsDir, skillDir);
|
|
82
|
+
|
|
83
|
+
// Skip if not a directory
|
|
84
|
+
if (!fs.statSync(skillPath).isDirectory()) continue;
|
|
85
|
+
|
|
86
|
+
const skillFile = path.join(skillPath, 'SKILL.md');
|
|
87
|
+
|
|
88
|
+
// Skip if SKILL.md doesn't exist
|
|
89
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
90
|
+
|
|
91
|
+
const frontmatter = extractFrontmatter(skillFile);
|
|
92
|
+
const name = frontmatter.name || skillDir;
|
|
93
|
+
const description = frontmatter.description || '';
|
|
94
|
+
|
|
95
|
+
skills.push({
|
|
96
|
+
name,
|
|
97
|
+
directory: skillDir,
|
|
98
|
+
file: 'SKILL.md',
|
|
99
|
+
path: skillFile,
|
|
100
|
+
description,
|
|
101
|
+
category: categorizeSkill(name, description)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Sort by category, then by name
|
|
106
|
+
skills.sort((a, b) => {
|
|
107
|
+
if (a.category !== b.category) {
|
|
108
|
+
return a.category.localeCompare(b.category);
|
|
109
|
+
}
|
|
110
|
+
return a.name.localeCompare(b.name);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return skills;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Main function
|
|
118
|
+
*/
|
|
119
|
+
function main() {
|
|
120
|
+
const rootDir = path.resolve(__dirname, '../..');
|
|
121
|
+
const skillsDir = path.join(rootDir, 'src/core/skills');
|
|
122
|
+
|
|
123
|
+
if (!fs.existsSync(skillsDir)) {
|
|
124
|
+
console.error(`Skills directory not found: ${skillsDir}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const skills = scanSkills(skillsDir);
|
|
129
|
+
|
|
130
|
+
// If called directly, output JSON
|
|
131
|
+
if (require.main === module) {
|
|
132
|
+
console.log(JSON.stringify(skills, null, 2));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return skills;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Export for use in other scripts
|
|
139
|
+
module.exports = { scanSkills, extractFrontmatter, categorizeSkill };
|
|
140
|
+
|
|
141
|
+
// Run if called directly
|
|
142
|
+
if (require.main === module) {
|
|
143
|
+
main();
|
|
144
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* get-env.js - Helper script to output environment information
|
|
5
|
+
*
|
|
6
|
+
* This script can be called from hooks or other automation to get
|
|
7
|
+
* consistent environment information about the AgileFlow project.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/get-env.js [--json] [--compact]
|
|
11
|
+
*
|
|
12
|
+
* Flags:
|
|
13
|
+
* --json Output as JSON
|
|
14
|
+
* --compact Minimal output for status line
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const { execSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
function getProjectInfo() {
|
|
23
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
24
|
+
|
|
25
|
+
// Read package.json files
|
|
26
|
+
let cliPackage = {};
|
|
27
|
+
let rootPackage = {};
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
cliPackage = JSON.parse(
|
|
31
|
+
fs.readFileSync(path.join(rootDir, 'packages/cli/package.json'), 'utf8')
|
|
32
|
+
);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
// Ignore if not found
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
rootPackage = JSON.parse(
|
|
39
|
+
fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8')
|
|
40
|
+
);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
// Ignore if not found
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Get git info
|
|
46
|
+
let gitBranch = 'unknown';
|
|
47
|
+
let gitCommit = 'unknown';
|
|
48
|
+
let recentCommits = [];
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
gitBranch = execSync('git branch --show-current', {
|
|
52
|
+
cwd: rootDir,
|
|
53
|
+
encoding: 'utf8'
|
|
54
|
+
}).trim();
|
|
55
|
+
gitCommit = execSync('git rev-parse --short HEAD', {
|
|
56
|
+
cwd: rootDir,
|
|
57
|
+
encoding: 'utf8'
|
|
58
|
+
}).trim();
|
|
59
|
+
|
|
60
|
+
// Get recent commits (last 5)
|
|
61
|
+
const commitLog = execSync('git log --oneline -5 2>/dev/null', {
|
|
62
|
+
cwd: rootDir,
|
|
63
|
+
encoding: 'utf8'
|
|
64
|
+
}).trim();
|
|
65
|
+
recentCommits = commitLog.split('\n').filter(Boolean);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// Ignore if git not available
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get AgileFlow status info
|
|
71
|
+
let activeStories = [];
|
|
72
|
+
let wipCount = 0;
|
|
73
|
+
let blockedCount = 0;
|
|
74
|
+
let activeEpics = [];
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const statusPath = path.join(rootDir, 'docs/09-agents/status.json');
|
|
78
|
+
if (fs.existsSync(statusPath)) {
|
|
79
|
+
const status = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
|
80
|
+
|
|
81
|
+
// Get active stories
|
|
82
|
+
if (status.stories) {
|
|
83
|
+
Object.entries(status.stories).forEach(([id, story]) => {
|
|
84
|
+
if (story.status === 'in_progress') {
|
|
85
|
+
activeStories.push({ id, title: story.title, owner: story.owner });
|
|
86
|
+
wipCount++;
|
|
87
|
+
}
|
|
88
|
+
if (story.status === 'blocked') {
|
|
89
|
+
blockedCount++;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Get active epics
|
|
95
|
+
if (status.epics) {
|
|
96
|
+
Object.entries(status.epics).forEach(([id, epic]) => {
|
|
97
|
+
if (epic.status !== 'complete') {
|
|
98
|
+
activeEpics.push({ id, title: epic.title });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
// Ignore if status.json not available
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
project: {
|
|
109
|
+
name: cliPackage.name || rootPackage.name || 'AgileFlow',
|
|
110
|
+
version: cliPackage.version || rootPackage.version || 'unknown',
|
|
111
|
+
description: cliPackage.description || rootPackage.description || '',
|
|
112
|
+
rootDir: rootDir,
|
|
113
|
+
},
|
|
114
|
+
git: {
|
|
115
|
+
branch: gitBranch,
|
|
116
|
+
commit: gitCommit,
|
|
117
|
+
recentCommits: recentCommits,
|
|
118
|
+
},
|
|
119
|
+
agileflow: {
|
|
120
|
+
activeStories: activeStories,
|
|
121
|
+
wipCount: wipCount,
|
|
122
|
+
blockedCount: blockedCount,
|
|
123
|
+
activeEpics: activeEpics,
|
|
124
|
+
},
|
|
125
|
+
system: {
|
|
126
|
+
node: process.version,
|
|
127
|
+
platform: os.platform(),
|
|
128
|
+
arch: os.arch(),
|
|
129
|
+
hostname: os.hostname(),
|
|
130
|
+
user: os.userInfo().username,
|
|
131
|
+
},
|
|
132
|
+
timestamp: new Date().toISOString(),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatOutput(info, asJson = false, compact = false) {
|
|
137
|
+
if (asJson) {
|
|
138
|
+
return JSON.stringify(info, null, 2);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (compact) {
|
|
142
|
+
// Minimal output for status line
|
|
143
|
+
const story = info.agileflow.activeStories[0];
|
|
144
|
+
const storyStr = story ? `${story.id}: ${story.title.substring(0, 30)}` : 'No active story';
|
|
145
|
+
return `[${info.git.branch}] ${storyStr} | WIP: ${info.agileflow.wipCount}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ANSI colors (including brand color #e8683a as RGB)
|
|
149
|
+
const c = {
|
|
150
|
+
reset: '\x1b[0m',
|
|
151
|
+
bold: '\x1b[1m',
|
|
152
|
+
dim: '\x1b[2m',
|
|
153
|
+
green: '\x1b[32m',
|
|
154
|
+
yellow: '\x1b[33m',
|
|
155
|
+
blue: '\x1b[34m',
|
|
156
|
+
cyan: '\x1b[36m',
|
|
157
|
+
red: '\x1b[31m',
|
|
158
|
+
brand: '\x1b[38;2;232;104;58m', // #e8683a - AgileFlow brand orange
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Beautiful compact colorful format
|
|
162
|
+
const lines = [];
|
|
163
|
+
|
|
164
|
+
// Header line with project info (brand color name, dim version, colored branch)
|
|
165
|
+
const branchColor = info.git.branch === 'main' ? c.green : c.cyan;
|
|
166
|
+
lines.push(`${c.brand}${c.bold}${info.project.name}${c.reset} ${c.dim}v${info.project.version}${c.reset} | ${branchColor}${info.git.branch}${c.reset} ${c.dim}(${info.git.commit})${c.reset}`);
|
|
167
|
+
|
|
168
|
+
// Status line (yellow WIP, red blocked)
|
|
169
|
+
const wipColor = info.agileflow.wipCount > 0 ? c.yellow : c.dim;
|
|
170
|
+
let statusLine = info.agileflow.wipCount > 0
|
|
171
|
+
? `${wipColor}WIP: ${info.agileflow.wipCount}${c.reset}`
|
|
172
|
+
: `${c.dim}No active work${c.reset}`;
|
|
173
|
+
if (info.agileflow.blockedCount > 0) {
|
|
174
|
+
statusLine += ` | ${c.red}Blocked: ${info.agileflow.blockedCount}${c.reset}`;
|
|
175
|
+
}
|
|
176
|
+
lines.push(statusLine);
|
|
177
|
+
|
|
178
|
+
// Active story (if any) - just the first one (blue label)
|
|
179
|
+
if (info.agileflow.activeStories.length > 0) {
|
|
180
|
+
const story = info.agileflow.activeStories[0];
|
|
181
|
+
lines.push(`${c.blue}Current:${c.reset} ${story.id} - ${story.title}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Last commit (just one, dim)
|
|
185
|
+
if (info.git.recentCommits.length > 0) {
|
|
186
|
+
lines.push(`${c.dim}Last: ${info.git.recentCommits[0]}${c.reset}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return lines.join('\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Main execution
|
|
193
|
+
if (require.main === module) {
|
|
194
|
+
const args = process.argv.slice(2);
|
|
195
|
+
const asJson = args.includes('--json');
|
|
196
|
+
const compact = args.includes('--compact');
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const info = getProjectInfo();
|
|
200
|
+
console.log(formatOutput(info, asJson, compact));
|
|
201
|
+
process.exit(0);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.error('Error getting environment info:', err.message);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Export for use as module
|
|
209
|
+
module.exports = { getProjectInfo, formatOutput };
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* obtain-context.js
|
|
4
|
+
*
|
|
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.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/obtain-context.js # Just gather context
|
|
11
|
+
* node scripts/obtain-context.js babysit # Gather + register 'babysit'
|
|
12
|
+
* node scripts/obtain-context.js mentor # Gather + register 'mentor'
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
// Optional: Register command for PreCompact context preservation
|
|
20
|
+
const commandName = process.argv[2];
|
|
21
|
+
if (commandName) {
|
|
22
|
+
const sessionStatePath = 'docs/09-agents/session-state.json';
|
|
23
|
+
if (fs.existsSync(sessionStatePath)) {
|
|
24
|
+
try {
|
|
25
|
+
const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
26
|
+
state.active_command = { name: commandName, activated_at: new Date().toISOString(), state: {} };
|
|
27
|
+
fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
|
|
28
|
+
} catch (e) {
|
|
29
|
+
// Silently continue if session state can't be updated
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ANSI colors
|
|
35
|
+
const C = {
|
|
36
|
+
reset: '\x1b[0m',
|
|
37
|
+
dim: '\x1b[2m',
|
|
38
|
+
bold: '\x1b[1m',
|
|
39
|
+
cyan: '\x1b[36m',
|
|
40
|
+
yellow: '\x1b[33m',
|
|
41
|
+
green: '\x1b[32m',
|
|
42
|
+
red: '\x1b[31m',
|
|
43
|
+
magenta: '\x1b[35m',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function safeRead(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function safeReadJSON(filePath) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function safeLs(dirPath) {
|
|
63
|
+
try {
|
|
64
|
+
return fs.readdirSync(dirPath);
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function safeExec(cmd) {
|
|
71
|
+
try {
|
|
72
|
+
return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
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
|
+
// ============================================
|
|
87
|
+
// MAIN CONTEXT GATHERING
|
|
88
|
+
// ============================================
|
|
89
|
+
|
|
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
|
+
}
|
|
110
|
+
|
|
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
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
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
|
+
});
|
|
135
|
+
|
|
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}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
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'})`);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
console.log(`${C.dim}No status.json found${C.reset}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
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}`);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
console.log(`${C.dim}No active session${C.reset}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
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}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Active command (for context preservation)
|
|
190
|
+
if (sessionState.active_command) {
|
|
191
|
+
console.log(`Active command: ${C.cyan}${sessionState.active_command.name}${C.reset}`);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
console.log(`${C.dim}No session-state.json found${C.reset}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
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;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
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'));
|
|
214
|
+
|
|
215
|
+
let info = [];
|
|
216
|
+
if (mdFiles.length > 0) info.push(`${mdFiles.length} md`);
|
|
217
|
+
if (jsonFiles.length > 0) info.push(`${jsonFiles.length} json`);
|
|
218
|
+
|
|
219
|
+
console.log(` ${C.dim}${folder}/${C.reset} ${info.length > 0 ? `(${info.join(', ')})` : ''}`);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
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
|
+
}
|
|
239
|
+
|
|
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
|
+
});
|
|
257
|
+
} else {
|
|
258
|
+
console.log(`${C.dim}No messages${C.reset}`);
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
console.log(`${C.dim}No bus log found${C.reset}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
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
|
+
});
|
|
287
|
+
} else {
|
|
288
|
+
console.log(`${C.dim}No epic files${C.reset}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
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`);
|