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.
Files changed (58) hide show
  1. package/README.md +229 -0
  2. package/bin/cli.js +186 -0
  3. package/examples/python-project/.agent-room.json +14 -0
  4. package/examples/python-project/AGENTS.md +32 -0
  5. package/examples/rust-project/.agent-room.json +12 -0
  6. package/examples/rust-project/AGENTS.md +32 -0
  7. package/lib/color.js +31 -0
  8. package/lib/fsutil.js +218 -0
  9. package/lib/init.js +660 -0
  10. package/lib/lint-sessions.js +278 -0
  11. package/lib/metrics.js +190 -0
  12. package/lib/pr.js +176 -0
  13. package/lib/prompt.js +20 -0
  14. package/lib/session-utils.js +213 -0
  15. package/lib/sync.js +138 -0
  16. package/lib/validate.js +179 -0
  17. package/package.json +48 -0
  18. package/templates/.agent-room/anti-patterns.md +22 -0
  19. package/templates/.agent-room/coordination/handoff-protocol.md +60 -0
  20. package/templates/.agent-room/coordination/scope-boundaries.md +57 -0
  21. package/templates/.agent-room/coordination/session-log-format.md +62 -0
  22. package/templates/.agent-room/decisions.md +17 -0
  23. package/templates/.agent-room/guardrails.json +23 -0
  24. package/templates/.agent-room/guardrails.md +56 -0
  25. package/templates/.agent-room/principles.md +306 -0
  26. package/templates/.agent-room/sessions/.gitkeep +4 -0
  27. package/templates/.agent-room/skills/brainstorming.md +64 -0
  28. package/templates/.agent-room/skills/closing-the-loop.md +67 -0
  29. package/templates/.agent-room/skills/systematic-debugging.md +85 -0
  30. package/templates/.agent-room/skills/test-driven-development.md +100 -0
  31. package/templates/.agent-room/skills/verification-before-completion.md +56 -0
  32. package/templates/.agent-room/skills/writing-plans.md +87 -0
  33. package/templates/.agent-room/workflow-classifier.md +127 -0
  34. package/templates/AGENTS.md.tmpl +85 -0
  35. package/templates/adapters/CLAUDE.md.tmpl +38 -0
  36. package/templates/adapters/claude-hooks/close-the-loop-check.js +96 -0
  37. package/templates/adapters/clinerules.tmpl +14 -0
  38. package/templates/adapters/codexrules.tmpl +45 -0
  39. package/templates/adapters/cursorrules.tmpl +14 -0
  40. package/templates/adapters/git-hooks/guardrails-check.js +140 -0
  41. package/templates/adapters/git-hooks/pre-commit.tmpl +43 -0
  42. package/templates/adapters/windsurfrules.tmpl +14 -0
  43. package/templates/docs/plans/.gitkeep +0 -0
  44. package/templates/skill-packs/api-design/api-design.md +152 -0
  45. package/templates/skill-packs/code-review/code-review.md +113 -0
  46. package/templates/skill-packs/database/database-migrations.md +123 -0
  47. package/templates/skill-packs/documentation/documentation.md +155 -0
  48. package/templates/skill-packs/observability/observability.md +128 -0
  49. package/templates/skill-packs/performance/performance-optimization.md +150 -0
  50. package/templates/skill-packs/release/release-management.md +145 -0
  51. package/templates/skill-packs/security/security-principles.md +127 -0
  52. package/templates/skill-packs/testing/integration-testing.md +127 -0
  53. package/templates/stacks/python/.agent-room/skills/python-testing.md +59 -0
  54. package/templates/stacks/python/AGENTS.md.tmpl +35 -0
  55. package/templates/stacks/react/.agent-room/skills/react-component-testing.md +76 -0
  56. package/templates/stacks/react/AGENTS.md.tmpl +37 -0
  57. package/templates/stacks/typescript/.agent-room/skills/typescript-testing.md +63 -0
  58. 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 };