create-agent-room 1.2.1
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/README.md +229 -0
- package/bin/cli.js +186 -0
- package/examples/python-project/.agent-room.json +14 -0
- package/examples/python-project/AGENTS.md +32 -0
- package/examples/rust-project/.agent-room.json +12 -0
- package/examples/rust-project/AGENTS.md +32 -0
- package/lib/color.js +31 -0
- package/lib/fsutil.js +218 -0
- package/lib/init.js +660 -0
- package/lib/lint-sessions.js +278 -0
- package/lib/metrics.js +190 -0
- package/lib/pr.js +176 -0
- package/lib/prompt.js +20 -0
- package/lib/session-utils.js +213 -0
- package/lib/sync.js +138 -0
- package/lib/validate.js +179 -0
- package/package.json +48 -0
- package/templates/.agent-room/anti-patterns.md +22 -0
- package/templates/.agent-room/coordination/handoff-protocol.md +60 -0
- package/templates/.agent-room/coordination/scope-boundaries.md +57 -0
- package/templates/.agent-room/coordination/session-log-format.md +62 -0
- package/templates/.agent-room/decisions.md +17 -0
- package/templates/.agent-room/guardrails.json +23 -0
- package/templates/.agent-room/guardrails.md +56 -0
- package/templates/.agent-room/principles.md +306 -0
- package/templates/.agent-room/sessions/.gitkeep +4 -0
- package/templates/.agent-room/skills/brainstorming.md +64 -0
- package/templates/.agent-room/skills/closing-the-loop.md +67 -0
- package/templates/.agent-room/skills/systematic-debugging.md +85 -0
- package/templates/.agent-room/skills/test-driven-development.md +100 -0
- package/templates/.agent-room/skills/verification-before-completion.md +56 -0
- package/templates/.agent-room/skills/writing-plans.md +87 -0
- package/templates/.agent-room/workflow-classifier.md +127 -0
- package/templates/AGENTS.md.tmpl +85 -0
- package/templates/adapters/CLAUDE.md.tmpl +38 -0
- package/templates/adapters/claude-hooks/close-the-loop-check.js +96 -0
- package/templates/adapters/clinerules.tmpl +14 -0
- package/templates/adapters/codexrules.tmpl +45 -0
- package/templates/adapters/cursorrules.tmpl +14 -0
- package/templates/adapters/git-hooks/guardrails-check.js +140 -0
- package/templates/adapters/git-hooks/pre-commit.tmpl +43 -0
- package/templates/adapters/windsurfrules.tmpl +14 -0
- package/templates/docs/plans/.gitkeep +0 -0
- package/templates/skill-packs/api-design/api-design.md +152 -0
- package/templates/skill-packs/code-review/code-review.md +113 -0
- package/templates/skill-packs/database/database-migrations.md +123 -0
- package/templates/skill-packs/documentation/documentation.md +155 -0
- package/templates/skill-packs/observability/observability.md +128 -0
- package/templates/skill-packs/performance/performance-optimization.md +150 -0
- package/templates/skill-packs/release/release-management.md +145 -0
- package/templates/skill-packs/security/security-principles.md +127 -0
- package/templates/skill-packs/testing/integration-testing.md +127 -0
- package/templates/stacks/python/.agent-room/skills/python-testing.md +59 -0
- package/templates/stacks/python/AGENTS.md.tmpl +35 -0
- package/templates/stacks/react/.agent-room/skills/react-component-testing.md +76 -0
- package/templates/stacks/react/AGENTS.md.tmpl +37 -0
- package/templates/stacks/typescript/.agent-room/skills/typescript-testing.md +63 -0
- package/templates/stacks/typescript/AGENTS.md.tmpl +36 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { green, yellow, red, cyan, bold } = require('./color');
|
|
6
|
+
|
|
7
|
+
const REQUIRED_SECTIONS = [
|
|
8
|
+
'Goal',
|
|
9
|
+
'Files touched',
|
|
10
|
+
'Actions taken',
|
|
11
|
+
'Tests run',
|
|
12
|
+
'Decisions made',
|
|
13
|
+
'Outcome'
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// These are inline fields, not section headers
|
|
17
|
+
const REQUIRED_FIELDS = ['Date', 'Agent', 'Classification'];
|
|
18
|
+
|
|
19
|
+
const VALID_CLASSIFICATIONS = ['Bug', 'Enhancement', 'Feature', 'Product'];
|
|
20
|
+
const VALID_OUTCOMES = ['Completed', 'Handed Off', 'In Progress', 'Blocked'];
|
|
21
|
+
|
|
22
|
+
function validateMarkdownSession(filePath, fileName) {
|
|
23
|
+
const errors = [];
|
|
24
|
+
const warnings = [];
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
28
|
+
|
|
29
|
+
// Check if file is empty or too short
|
|
30
|
+
if (!content.trim()) {
|
|
31
|
+
errors.push(`${fileName}: File is empty`);
|
|
32
|
+
return { errors, warnings };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check for session log title
|
|
36
|
+
if (!content.match(/^#\s+Session Log:/m)) {
|
|
37
|
+
errors.push(`${fileName}: Missing "# Session Log:" header`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check for required inline fields (Date, Agent, Classification)
|
|
41
|
+
for (const field of REQUIRED_FIELDS) {
|
|
42
|
+
const fieldRegex = new RegExp(`\\*\\*${field}:\\*\\*\\s*(.+?)(?:\\n|\\r|$)`, 'i');
|
|
43
|
+
if (!content.match(fieldRegex)) {
|
|
44
|
+
errors.push(`${fileName}: Missing required field "**${field}:**"`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check for each required section header
|
|
49
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
50
|
+
const sectionRegex = new RegExp(`^##\\s+${section}`, 'm');
|
|
51
|
+
if (!content.match(sectionRegex)) {
|
|
52
|
+
errors.push(`${fileName}: Missing required section "## ${section}"`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Validate Date format
|
|
57
|
+
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+?)(?:\n|\r)/);
|
|
58
|
+
if (!dateMatch || !dateMatch[1].trim()) {
|
|
59
|
+
errors.push(`${fileName}: Date field is empty or malformed`);
|
|
60
|
+
} else {
|
|
61
|
+
const dateStr = dateMatch[1].trim();
|
|
62
|
+
if (!/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}/.test(dateStr)) {
|
|
63
|
+
warnings.push(`${fileName}: Date format should be "YYYY-MM-DD HH:MM", got "${dateStr}"`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Validate Agent field
|
|
68
|
+
const agentMatch = content.match(/\*\*Agent:\*\*\s*(.+?)(?:\n|\r)/);
|
|
69
|
+
if (!agentMatch || !agentMatch[1].trim()) {
|
|
70
|
+
errors.push(`${fileName}: Agent field is empty`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Validate Classification
|
|
74
|
+
const classMatch = content.match(/\*\*Classification:\*\*\s*(.+?)(?:\n|\r)/);
|
|
75
|
+
if (!classMatch || !classMatch[1].trim()) {
|
|
76
|
+
errors.push(`${fileName}: Classification field is empty`);
|
|
77
|
+
} else {
|
|
78
|
+
const classification = classMatch[1].trim();
|
|
79
|
+
if (!VALID_CLASSIFICATIONS.includes(classification)) {
|
|
80
|
+
errors.push(`${fileName}: Invalid Classification "${classification}". Must be one of: ${VALID_CLASSIFICATIONS.join(', ')}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Validate Goal (one sentence, not empty)
|
|
85
|
+
const goalMatch = content.match(/##\s+Goal\r?\n+([\s\S]*?)(?=##|$)/);
|
|
86
|
+
if (!goalMatch || !goalMatch[1].trim()) {
|
|
87
|
+
errors.push(`${fileName}: Goal section is empty`);
|
|
88
|
+
} else {
|
|
89
|
+
const goalText = goalMatch[1].trim();
|
|
90
|
+
if (goalText.split('\n').length > 1) {
|
|
91
|
+
warnings.push(`${fileName}: Goal should be one sentence, found multiple lines`);
|
|
92
|
+
}
|
|
93
|
+
if (goalText.length > 200) {
|
|
94
|
+
warnings.push(`${fileName}: Goal is very long (${goalText.length} chars), consider shorter wording`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Validate Files touched (should list something)
|
|
99
|
+
const filesMatch = content.match(/##\s+Files touched\r?\n+([\s\S]*?)(?=##|$)/);
|
|
100
|
+
if (!filesMatch || !filesMatch[1].trim()) {
|
|
101
|
+
warnings.push(`${fileName}: Files touched section is empty (should list Read/Created/Modified)`);
|
|
102
|
+
} else {
|
|
103
|
+
const filesText = filesMatch[1].trim();
|
|
104
|
+
if (!filesText.match(/-(Read|Created|Modified):/i)) {
|
|
105
|
+
warnings.push(`${fileName}: Files touched should use format "- Read: file.js" / "- Created: file.js" / "- Modified: file.js"`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Validate Actions taken (should have at least one action)
|
|
110
|
+
const actionsMatch = content.match(/##\s+Actions taken\r?\n+([\s\S]*?)(?=##|$)/);
|
|
111
|
+
if (!actionsMatch || !actionsMatch[1].trim()) {
|
|
112
|
+
errors.push(`${fileName}: Actions taken section is empty`);
|
|
113
|
+
} else {
|
|
114
|
+
const actionsText = actionsMatch[1].trim();
|
|
115
|
+
if (!actionsText.match(/^\s*\d+\./m)) {
|
|
116
|
+
warnings.push(`${fileName}: Actions should be numbered list (1., 2., etc.)`);
|
|
117
|
+
}
|
|
118
|
+
const actionCount = (actionsText.match(/^\s*\d+\./gm) || []).length;
|
|
119
|
+
if (actionCount === 0) {
|
|
120
|
+
errors.push(`${fileName}: Actions taken should contain numbered steps (found 0)`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Validate Tests run
|
|
125
|
+
const testsMatch = content.match(/##\s+Tests run\r?\n+([\s\S]*?)(?=##|$)/);
|
|
126
|
+
if (!testsMatch || !testsMatch[1].trim()) {
|
|
127
|
+
warnings.push(`${fileName}: Tests run section is empty`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Validate Outcome Status
|
|
131
|
+
const outcomeMatch = content.match(/##\s+Outcome[^#]*\*\*Status:\*\*\s*(.+?)(?:\n|\r)/);
|
|
132
|
+
if (!outcomeMatch || !outcomeMatch[1].trim()) {
|
|
133
|
+
warnings.push(`${fileName}: Outcome Status field is empty or malformed`);
|
|
134
|
+
} else {
|
|
135
|
+
const outcome = outcomeMatch[1].trim();
|
|
136
|
+
if (!VALID_OUTCOMES.includes(outcome)) {
|
|
137
|
+
warnings.push(`${fileName}: Outcome Status should be one of: ${VALID_OUTCOMES.join(', ')}, got "${outcome}"`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { errors, warnings };
|
|
142
|
+
} catch (err) {
|
|
143
|
+
return {
|
|
144
|
+
errors: [`${fileName}: Failed to read or parse: ${err.message}`],
|
|
145
|
+
warnings: []
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function validateJSONSession(filePath, fileName) {
|
|
151
|
+
const errors = [];
|
|
152
|
+
const warnings = [];
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
156
|
+
const data = JSON.parse(content);
|
|
157
|
+
|
|
158
|
+
// Check required fields
|
|
159
|
+
const requiredFields = ['date', 'agent', 'classification', 'goal', 'outcome'];
|
|
160
|
+
for (const field of requiredFields) {
|
|
161
|
+
if (!data[field]) {
|
|
162
|
+
errors.push(`${fileName}: Missing required field "${field}"`);
|
|
163
|
+
} else if (typeof data[field] === 'string' && !data[field].trim()) {
|
|
164
|
+
errors.push(`${fileName}: Required field "${field}" is empty`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Validate classification
|
|
169
|
+
if (data.classification && !VALID_CLASSIFICATIONS.includes(data.classification)) {
|
|
170
|
+
errors.push(`${fileName}: Invalid classification "${data.classification}". Must be one of: ${VALID_CLASSIFICATIONS.join(', ')}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Validate outcome
|
|
174
|
+
if (data.outcome && !VALID_OUTCOMES.includes(data.outcome)) {
|
|
175
|
+
warnings.push(`${fileName}: Outcome should be one of: ${VALID_OUTCOMES.join(', ')}, got "${data.outcome}"`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check actions
|
|
179
|
+
if (!data.actions || (Array.isArray(data.actions) && data.actions.length === 0)) {
|
|
180
|
+
errors.push(`${fileName}: Actions should be a non-empty array`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { errors, warnings };
|
|
184
|
+
} catch (err) {
|
|
185
|
+
return {
|
|
186
|
+
errors: [`${fileName}: Failed to parse JSON: ${err.message}`],
|
|
187
|
+
warnings: []
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function runLintSessions(target) {
|
|
193
|
+
console.log(bold(`Linting session logs in: ${cyan(target)}\n`));
|
|
194
|
+
|
|
195
|
+
const sessionsDir = path.join(target, '.agent-room', 'sessions');
|
|
196
|
+
|
|
197
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
198
|
+
console.log(yellow(`⚠️ No sessions directory found at ${sessionsDir}`));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const files = fs.readdirSync(sessionsDir).filter((f) => f.endsWith('.md') || f.endsWith('.json'));
|
|
203
|
+
|
|
204
|
+
if (files.length === 0) {
|
|
205
|
+
console.log(yellow(`⚠️ No session logs found in ${sessionsDir}`));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let totalErrors = 0;
|
|
210
|
+
let totalWarnings = 0;
|
|
211
|
+
const results = [];
|
|
212
|
+
|
|
213
|
+
for (const file of files) {
|
|
214
|
+
const filePath = path.join(sessionsDir, file);
|
|
215
|
+
let result;
|
|
216
|
+
|
|
217
|
+
if (file.endsWith('.md')) {
|
|
218
|
+
result = validateMarkdownSession(filePath, file);
|
|
219
|
+
} else if (file.endsWith('.json')) {
|
|
220
|
+
result = validateJSONSession(filePath, file);
|
|
221
|
+
} else {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
totalErrors += result.errors.length;
|
|
226
|
+
totalWarnings += result.warnings.length;
|
|
227
|
+
|
|
228
|
+
results.push({ file, ...result });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Output results
|
|
232
|
+
let hasFailed = false;
|
|
233
|
+
|
|
234
|
+
for (const result of results) {
|
|
235
|
+
if (result.errors.length > 0 || result.warnings.length > 0) {
|
|
236
|
+
console.log(`${bold(result.file)}`);
|
|
237
|
+
|
|
238
|
+
if (result.errors.length > 0) {
|
|
239
|
+
hasFailed = true;
|
|
240
|
+
for (const err of result.errors) {
|
|
241
|
+
console.log(red(` ❌ ${err}`));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (result.warnings.length > 0) {
|
|
246
|
+
for (const warn of result.warnings) {
|
|
247
|
+
console.log(yellow(` ⚠️ ${warn}`));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log('');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Summary
|
|
256
|
+
console.log(bold('Summary:'));
|
|
257
|
+
console.log(` Files scanned: ${files.length}`);
|
|
258
|
+
console.log(` Errors: ${totalErrors}`);
|
|
259
|
+
console.log(` Warnings: ${totalWarnings}`);
|
|
260
|
+
|
|
261
|
+
if (hasFailed) {
|
|
262
|
+
console.log('');
|
|
263
|
+
console.log(red('Linting FAILED: Some session logs have validation errors.'));
|
|
264
|
+
process.exitCode = 1;
|
|
265
|
+
} else if (totalWarnings > 0) {
|
|
266
|
+
console.log('');
|
|
267
|
+
console.log(bold(yellow('Linting PASSED with warnings.')));
|
|
268
|
+
} else {
|
|
269
|
+
console.log('');
|
|
270
|
+
console.log(bold(green('Linting PASSED! All session logs are valid.')));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = {
|
|
275
|
+
runLintSessions,
|
|
276
|
+
validateMarkdownSession,
|
|
277
|
+
validateJSONSession
|
|
278
|
+
};
|
package/lib/metrics.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { green, yellow, red, cyan, bold } = require('./color');
|
|
6
|
+
|
|
7
|
+
function countFiles(str) {
|
|
8
|
+
if (!str) return 0;
|
|
9
|
+
const clean = str.trim().replace(/\[|\]/g, '');
|
|
10
|
+
if (/^(none|blank|n\/a|list of.*|placeholder)$/i.test(clean)) return 0;
|
|
11
|
+
const items = clean.split(',').map((s) => s.trim()).filter(Boolean);
|
|
12
|
+
return items.length;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseFileList(val) {
|
|
16
|
+
if (Array.isArray(val)) return val.length;
|
|
17
|
+
if (typeof val === 'number') return val;
|
|
18
|
+
if (typeof val === 'string') return countFiles(val);
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseMarkdownSession(filePath, stats) {
|
|
23
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
24
|
+
stats.total++;
|
|
25
|
+
|
|
26
|
+
// Regex matches
|
|
27
|
+
const agentMatch = content.match(/\*\*Agent:\*\*\s*(.+)/i);
|
|
28
|
+
const classMatch = content.match(/\*\*Classification:\*\*\s*(.+)/i);
|
|
29
|
+
|
|
30
|
+
// Parse Outcome
|
|
31
|
+
let outcome = 'Handed Off';
|
|
32
|
+
const outcomeHeaderMatch = content.match(/## Outcome\s*\n+([^\n#\s]+[^\n#]*)/i);
|
|
33
|
+
if (outcomeHeaderMatch) {
|
|
34
|
+
const parsed = outcomeHeaderMatch[1].trim();
|
|
35
|
+
if (/completed/i.test(parsed)) outcome = 'Completed';
|
|
36
|
+
else if (/blocked/i.test(parsed)) outcome = 'Blocked';
|
|
37
|
+
else if (/handed/i.test(parsed)) outcome = 'Handed Off';
|
|
38
|
+
}
|
|
39
|
+
stats.outcomes[outcome]++;
|
|
40
|
+
|
|
41
|
+
if (classMatch) {
|
|
42
|
+
const val = classMatch[1].trim();
|
|
43
|
+
let matchedClass = null;
|
|
44
|
+
if (/bug/i.test(val)) matchedClass = 'Bug';
|
|
45
|
+
else if (/enhancement/i.test(val)) matchedClass = 'Enhancement';
|
|
46
|
+
else if (/feature/i.test(val)) matchedClass = 'Feature';
|
|
47
|
+
else if (/product/i.test(val)) matchedClass = 'Product';
|
|
48
|
+
|
|
49
|
+
if (matchedClass) {
|
|
50
|
+
stats.classifications[matchedClass]++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (agentMatch) {
|
|
55
|
+
const agent = agentMatch[1].trim().replace(/\[|\]/g, '');
|
|
56
|
+
stats.agents[agent] = (stats.agents[agent] || 0) + 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Parse Files touched
|
|
60
|
+
const readMatch = content.match(/-\s*Read:\s*(.+)/i);
|
|
61
|
+
const createdMatch = content.match(/-\s*Created:\s*(.+)/i);
|
|
62
|
+
const modifiedMatch = content.match(/-\s*Modified:\s*(.+)/i);
|
|
63
|
+
|
|
64
|
+
if (readMatch) stats.filesRead += countFiles(readMatch[1]);
|
|
65
|
+
if (createdMatch) stats.filesCreated += countFiles(createdMatch[1]);
|
|
66
|
+
if (modifiedMatch) stats.filesModified += countFiles(modifiedMatch[1]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseJSONSession(filePath, stats) {
|
|
70
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
71
|
+
const data = JSON.parse(content);
|
|
72
|
+
stats.total++;
|
|
73
|
+
|
|
74
|
+
if (data.classification) {
|
|
75
|
+
const val = data.classification;
|
|
76
|
+
if (stats.classifications[val] !== undefined) {
|
|
77
|
+
stats.classifications[val]++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (data.outcome) {
|
|
81
|
+
let outcome = 'Handed Off';
|
|
82
|
+
if (/completed/i.test(data.outcome)) outcome = 'Completed';
|
|
83
|
+
else if (/blocked/i.test(data.outcome)) outcome = 'Blocked';
|
|
84
|
+
stats.outcomes[outcome]++;
|
|
85
|
+
} else {
|
|
86
|
+
stats.outcomes['Handed Off']++;
|
|
87
|
+
}
|
|
88
|
+
if (data.agent) {
|
|
89
|
+
stats.agents[data.agent] = (stats.agents[data.agent] || 0) + 1;
|
|
90
|
+
}
|
|
91
|
+
if (data.filesTouched) {
|
|
92
|
+
if (data.filesTouched.read) stats.filesRead += parseFileList(data.filesTouched.read);
|
|
93
|
+
if (data.filesTouched.created) stats.filesCreated += parseFileList(data.filesTouched.created);
|
|
94
|
+
if (data.filesTouched.modified) stats.filesModified += parseFileList(data.filesTouched.modified);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function printDashboard(stats) {
|
|
99
|
+
const t = stats.total;
|
|
100
|
+
const pct = (val) => (t > 0 ? ((val / t) * 100).toFixed(1) : '0.0');
|
|
101
|
+
|
|
102
|
+
const bar = (val) => {
|
|
103
|
+
if (t === 0) return '[ ]';
|
|
104
|
+
const fillCount = Math.round((val / t) * 20);
|
|
105
|
+
const fill = '='.repeat(Math.max(0, fillCount - 1)) + (fillCount > 0 ? '>' : '');
|
|
106
|
+
const empty = ' '.repeat(20 - fillCount);
|
|
107
|
+
return `[${fill}${empty}]`;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
console.log(bold('======================================================'));
|
|
111
|
+
console.log(bold(' Agent Session Dashboard'));
|
|
112
|
+
console.log(bold('======================================================'));
|
|
113
|
+
console.log(`Total Sessions Logged: ${cyan(t)}\n`);
|
|
114
|
+
|
|
115
|
+
console.log(bold('--- Outcomes Success Rate ---'));
|
|
116
|
+
console.log(`Completed: ${green(bar(stats.outcomes.Completed))} ${pct(stats.outcomes.Completed)}% (${stats.outcomes.Completed})`);
|
|
117
|
+
console.log(`Blocked: ${red(bar(stats.outcomes.Blocked))} ${pct(stats.outcomes.Blocked)}% (${stats.outcomes.Blocked})`);
|
|
118
|
+
console.log(`Handed Off: ${yellow(bar(stats.outcomes['Handed Off']))} ${pct(stats.outcomes['Handed Off'])}% (${stats.outcomes['Handed Off']})\n`);
|
|
119
|
+
|
|
120
|
+
console.log(bold('--- Classification Distribution ---'));
|
|
121
|
+
console.log(`Bug: ${stats.classifications.Bug} (${pct(stats.classifications.Bug)}%)`);
|
|
122
|
+
console.log(`Enhancement: ${stats.classifications.Enhancement} (${pct(stats.classifications.Enhancement)}%)`);
|
|
123
|
+
console.log(`Feature: ${stats.classifications.Feature} (${pct(stats.classifications.Feature)}%)`);
|
|
124
|
+
console.log(`Product: ${stats.classifications.Product} (${pct(stats.classifications.Product)}%)\n`);
|
|
125
|
+
|
|
126
|
+
console.log(bold('--- Agent Breakdown ---'));
|
|
127
|
+
const agentsSorted = Object.entries(stats.agents).sort((a, b) => b[1] - a[1]);
|
|
128
|
+
if (agentsSorted.length === 0) {
|
|
129
|
+
console.log('No agent information logged.');
|
|
130
|
+
} else {
|
|
131
|
+
for (const [agent, count] of agentsSorted) {
|
|
132
|
+
console.log(`- ${agent}: ${count} session(s)`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
console.log('');
|
|
136
|
+
|
|
137
|
+
console.log(bold('--- File Volumes ---'));
|
|
138
|
+
console.log(`Files Read: ${stats.filesRead}`);
|
|
139
|
+
console.log(`Files Created: ${stats.filesCreated}`);
|
|
140
|
+
console.log(`Files Modified: ${stats.filesModified}`);
|
|
141
|
+
console.log(bold('======================================================'));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function runMetrics(target) {
|
|
145
|
+
const sessionsDir = path.join(target, '.agent-room', 'sessions');
|
|
146
|
+
if (!fs.existsSync(sessionsDir) || !fs.statSync(sessionsDir).isDirectory()) {
|
|
147
|
+
console.log(yellow(`No sessions directory found at ${path.relative(process.cwd(), sessionsDir)}`));
|
|
148
|
+
console.log('Ensure agents have logged sessions under .agent-room/sessions/ first.');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const files = fs.readdirSync(sessionsDir).filter(
|
|
153
|
+
(f) => (f.endsWith('.md') || f.endsWith('.json')) && f !== '.gitkeep'
|
|
154
|
+
);
|
|
155
|
+
if (files.length === 0) {
|
|
156
|
+
console.log(yellow('No session logs found.'));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const stats = {
|
|
161
|
+
total: 0,
|
|
162
|
+
classifications: { Bug: 0, Enhancement: 0, Feature: 0, Product: 0 },
|
|
163
|
+
outcomes: { Completed: 0, Blocked: 0, 'Handed Off': 0 },
|
|
164
|
+
agents: {},
|
|
165
|
+
filesRead: 0,
|
|
166
|
+
filesCreated: 0,
|
|
167
|
+
filesModified: 0
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
for (const file of files) {
|
|
171
|
+
const fullPath = path.join(sessionsDir, file);
|
|
172
|
+
try {
|
|
173
|
+
if (file.endsWith('.json')) {
|
|
174
|
+
parseJSONSession(fullPath, stats);
|
|
175
|
+
} else {
|
|
176
|
+
parseMarkdownSession(fullPath, stats);
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.warn(yellow(`Warning: Failed to parse session log ${file}: ${err.message}`));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
printDashboard(stats);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
runMetrics,
|
|
188
|
+
parseMarkdownSession,
|
|
189
|
+
parseJSONSession
|
|
190
|
+
};
|
package/lib/pr.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { green, red } = require('./color');
|
|
6
|
+
|
|
7
|
+
function getSection(content, headerName) {
|
|
8
|
+
const escapedHeader = headerName.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
9
|
+
const regex = new RegExp(`##\\s*${escapedHeader}\\s*\\r?\\n([\\s\\S]*?)(?:\\r?\\n##|$)`, 'i');
|
|
10
|
+
const match = content.match(regex);
|
|
11
|
+
return match ? match[1].trim() : '';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseMarkdownSession(filePath) {
|
|
15
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
16
|
+
|
|
17
|
+
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/i);
|
|
18
|
+
const agentMatch = content.match(/\*\*Agent:\*\*\s*(.+)/i);
|
|
19
|
+
const classMatch = content.match(/\*\*Classification:\*\*\s*(.+)/i);
|
|
20
|
+
|
|
21
|
+
const goal = getSection(content, 'Goal');
|
|
22
|
+
const filesTouched = getSection(content, 'Files touched');
|
|
23
|
+
const actions = getSection(content, 'Actions taken');
|
|
24
|
+
const tests = getSection(content, 'Tests run');
|
|
25
|
+
const decisions = getSection(content, 'Decisions made');
|
|
26
|
+
const outcomeRaw = getSection(content, 'Outcome');
|
|
27
|
+
|
|
28
|
+
// Parse Handoff note specifically
|
|
29
|
+
const handoffRegex = /\*\*Handoff note[^:]*:\*\*\r?\n?([\s\S]*)/i;
|
|
30
|
+
const handoffMatch = content.match(handoffRegex);
|
|
31
|
+
const handoffNote = handoffMatch ? handoffMatch[1].trim() : '';
|
|
32
|
+
|
|
33
|
+
// Clean outcome of handoff note text
|
|
34
|
+
let outcome = outcomeRaw;
|
|
35
|
+
const handoffHeaderMatch = outcomeRaw.match(/\*\*Handoff note/i);
|
|
36
|
+
if (handoffHeaderMatch) {
|
|
37
|
+
outcome = outcomeRaw.slice(0, handoffHeaderMatch.index).trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
date: dateMatch ? dateMatch[1].trim() : 'N/A',
|
|
42
|
+
agent: agentMatch ? agentMatch[1].trim().replace(/\[|\]/g, '') : 'N/A',
|
|
43
|
+
classification: classMatch ? classMatch[1].trim() : 'N/A',
|
|
44
|
+
goal,
|
|
45
|
+
filesTouched,
|
|
46
|
+
actions,
|
|
47
|
+
tests,
|
|
48
|
+
decisions,
|
|
49
|
+
outcome: outcome || 'Completed',
|
|
50
|
+
handoffNote
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseJSONSession(filePath) {
|
|
55
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
56
|
+
const data = JSON.parse(content);
|
|
57
|
+
|
|
58
|
+
const formatFileList = (files) => {
|
|
59
|
+
if (Array.isArray(files)) return files.map((f) => `- ${f}`).join('\n');
|
|
60
|
+
if (typeof files === 'object' && files !== null) {
|
|
61
|
+
const parts = [];
|
|
62
|
+
if (files.read) parts.push(`- Read: ${Array.isArray(files.read) ? files.read.join(', ') : files.read}`);
|
|
63
|
+
if (files.created) parts.push(`- Created: ${Array.isArray(files.created) ? files.created.join(', ') : files.created}`);
|
|
64
|
+
if (files.modified) parts.push(`- Modified: ${Array.isArray(files.modified) ? files.modified.join(', ') : files.modified}`);
|
|
65
|
+
return parts.join('\n');
|
|
66
|
+
}
|
|
67
|
+
return String(files || '');
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const formatActions = (actions) => {
|
|
71
|
+
if (Array.isArray(actions)) return actions.map((a, i) => `${i + 1}. ${a}`).join('\n');
|
|
72
|
+
return String(actions || '');
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
date: data.date || 'N/A',
|
|
77
|
+
agent: data.agent || 'N/A',
|
|
78
|
+
classification: data.classification || 'N/A',
|
|
79
|
+
goal: data.goal || '',
|
|
80
|
+
filesTouched: formatFileList(data.filesTouched),
|
|
81
|
+
actions: formatActions(data.actions),
|
|
82
|
+
tests: data.testsRun ? `Command: ${data.testsRun.command || ''}\nResult: ${data.testsRun.result || ''}` : '',
|
|
83
|
+
decisions: Array.isArray(data.decisions) ? data.decisions.map((d) => `- ${d}`).join('\n') : String(data.decisions || ''),
|
|
84
|
+
outcome: data.outcome || 'Completed',
|
|
85
|
+
handoffNote: data.handoffNote || ''
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function runPrDesc(target, args) {
|
|
90
|
+
const sessionsDir = path.join(target, '.agent-room', 'sessions');
|
|
91
|
+
if (!fs.existsSync(sessionsDir) || !fs.statSync(sessionsDir).isDirectory()) {
|
|
92
|
+
console.error(red(`Error: Sessions directory not found at ${sessionsDir}`));
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const files = fs.readdirSync(sessionsDir).filter(
|
|
98
|
+
(f) => (f.endsWith('.md') || f.endsWith('.json')) && f !== '.gitkeep'
|
|
99
|
+
);
|
|
100
|
+
if (files.length === 0) {
|
|
101
|
+
console.error(red('Error: No session logs found.'));
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Sort alphabetically to find the latest session log
|
|
107
|
+
files.sort();
|
|
108
|
+
const latestFile = files[files.length - 1];
|
|
109
|
+
const fullPath = path.join(sessionsDir, latestFile);
|
|
110
|
+
|
|
111
|
+
let session = null;
|
|
112
|
+
try {
|
|
113
|
+
if (latestFile.endsWith('.json')) {
|
|
114
|
+
session = parseJSONSession(fullPath);
|
|
115
|
+
} else {
|
|
116
|
+
session = parseMarkdownSession(fullPath);
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error(red(`Error: Failed to parse session log ${latestFile}: ${err.message}`));
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Generate PR Description Markdown
|
|
125
|
+
let prDesc = `# Pull Request Description
|
|
126
|
+
|
|
127
|
+
## Overview
|
|
128
|
+
* **Session Log Reference:** [${latestFile}](.agent-room/sessions/${latestFile})
|
|
129
|
+
* **Date:** ${session.date}
|
|
130
|
+
* **Agent:** ${session.agent}
|
|
131
|
+
* **Classification:** ${session.classification}
|
|
132
|
+
|
|
133
|
+
## Goal
|
|
134
|
+
${session.goal || 'No goal documented.'}
|
|
135
|
+
|
|
136
|
+
## Changes Implemented
|
|
137
|
+
${session.filesTouched || 'No files touched documented.'}
|
|
138
|
+
|
|
139
|
+
## Actions Taken
|
|
140
|
+
${session.actions || 'No actions documented.'}
|
|
141
|
+
|
|
142
|
+
## Verification & Testing
|
|
143
|
+
${session.tests || 'No verification tests documented.'}
|
|
144
|
+
|
|
145
|
+
## Decisions & Architecture Changes
|
|
146
|
+
${session.decisions || 'No decisions documented.'}
|
|
147
|
+
|
|
148
|
+
## Outcome & Next Steps
|
|
149
|
+
* **Status:** ${session.outcome}
|
|
150
|
+
`;
|
|
151
|
+
|
|
152
|
+
if (session.handoffNote) {
|
|
153
|
+
prDesc += `\n### Handoff Details\n${session.handoffNote}\n`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Print to stdout
|
|
157
|
+
console.log(prDesc);
|
|
158
|
+
|
|
159
|
+
// Optionally write to .agent-room/pr-description.md
|
|
160
|
+
if (args.write) {
|
|
161
|
+
const outputPath = path.join(target, '.agent-room', 'pr-description.md');
|
|
162
|
+
try {
|
|
163
|
+
fs.writeFileSync(outputPath, prDesc);
|
|
164
|
+
console.log(green(`\nSuccess: PR description written to ${outputPath}`));
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error(red(`Error: Failed to write PR description to ${outputPath}: ${err.message}`));
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
runPrDesc,
|
|
174
|
+
parseMarkdownSession,
|
|
175
|
+
parseJSONSession
|
|
176
|
+
};
|
package/lib/prompt.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
|
|
5
|
+
function ask(question) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
rl.on('SIGINT', () => {
|
|
9
|
+
rl.close();
|
|
10
|
+
console.log('\nScaffolding cancelled. Goodbye!');
|
|
11
|
+
process.exit(0);
|
|
12
|
+
});
|
|
13
|
+
rl.question(question, (answer) => {
|
|
14
|
+
rl.close();
|
|
15
|
+
resolve(answer.trim());
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { ask };
|