@howlil/ez-agents 3.4.1 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +84 -20
  3. package/agents/ez-observer-agent.md +260 -0
  4. package/agents/ez-release-agent.md +333 -0
  5. package/agents/ez-requirements-agent.md +377 -0
  6. package/agents/ez-scrum-master-agent.md +242 -0
  7. package/agents/ez-tech-lead-agent.md +267 -0
  8. package/bin/install.js +3221 -3230
  9. package/commands/ez/arch-review.md +102 -0
  10. package/commands/ez/execute-phase.md +11 -0
  11. package/commands/ez/export-session.md +79 -0
  12. package/commands/ez/gather-requirements.md +117 -0
  13. package/commands/ez/git-workflow.md +72 -0
  14. package/commands/ez/hotfix.md +120 -0
  15. package/commands/ez/import-session.md +82 -0
  16. package/commands/ez/join-discord.md +18 -18
  17. package/commands/ez/list-sessions.md +96 -0
  18. package/commands/ez/package-manager.md +316 -0
  19. package/commands/ez/plan-phase.md +9 -1
  20. package/commands/ez/preflight.md +79 -0
  21. package/commands/ez/progress.md +13 -1
  22. package/commands/ez/release.md +153 -0
  23. package/commands/ez/resume.md +107 -0
  24. package/commands/ez/standup.md +85 -0
  25. package/ez-agents/bin/ez-tools.cjs +1095 -716
  26. package/ez-agents/bin/lib/assistant-adapter.cjs +264 -264
  27. package/ez-agents/bin/lib/audit-exec.cjs +7 -2
  28. package/ez-agents/bin/lib/bdd-validator.cjs +622 -0
  29. package/ez-agents/bin/lib/circuit-breaker.cjs +118 -118
  30. package/ez-agents/bin/lib/config.cjs +190 -190
  31. package/ez-agents/bin/lib/content-scanner.cjs +238 -0
  32. package/ez-agents/bin/lib/context-cache.cjs +154 -0
  33. package/ez-agents/bin/lib/context-errors.cjs +71 -0
  34. package/ez-agents/bin/lib/context-manager.cjs +220 -0
  35. package/ez-agents/bin/lib/discussion-synthesizer.cjs +458 -0
  36. package/ez-agents/bin/lib/file-access.cjs +207 -0
  37. package/ez-agents/bin/lib/file-lock.cjs +236 -236
  38. package/ez-agents/bin/lib/frontmatter.cjs +299 -299
  39. package/ez-agents/bin/lib/fs-utils.cjs +153 -153
  40. package/ez-agents/bin/lib/git-errors.cjs +83 -0
  41. package/ez-agents/bin/lib/git-utils.cjs +118 -0
  42. package/ez-agents/bin/lib/git-workflow-engine.cjs +1157 -0
  43. package/ez-agents/bin/lib/index.cjs +157 -113
  44. package/ez-agents/bin/lib/init.cjs +757 -757
  45. package/ez-agents/bin/lib/lockfile-validator.cjs +227 -0
  46. package/ez-agents/bin/lib/logger.cjs +124 -124
  47. package/ez-agents/bin/lib/memory-compression.cjs +256 -0
  48. package/ez-agents/bin/lib/metrics-tracker.cjs +406 -0
  49. package/ez-agents/bin/lib/milestone.cjs +241 -241
  50. package/ez-agents/bin/lib/model-provider.cjs +241 -241
  51. package/ez-agents/bin/lib/package-manager-detector.cjs +203 -0
  52. package/ez-agents/bin/lib/package-manager-executor.cjs +385 -0
  53. package/ez-agents/bin/lib/package-manager-service.cjs +216 -0
  54. package/ez-agents/bin/lib/phase.cjs +925 -925
  55. package/ez-agents/bin/lib/planning-write.cjs +107 -107
  56. package/ez-agents/bin/lib/release-validator.cjs +614 -0
  57. package/ez-agents/bin/lib/retry.cjs +119 -119
  58. package/ez-agents/bin/lib/roadmap.cjs +306 -306
  59. package/ez-agents/bin/lib/safe-exec.cjs +128 -128
  60. package/ez-agents/bin/lib/safe-path.cjs +130 -130
  61. package/ez-agents/bin/lib/session-chain.cjs +304 -0
  62. package/ez-agents/bin/lib/session-errors.cjs +81 -0
  63. package/ez-agents/bin/lib/session-export.cjs +251 -0
  64. package/ez-agents/bin/lib/session-import.cjs +262 -0
  65. package/ez-agents/bin/lib/session-manager.cjs +280 -0
  66. package/ez-agents/bin/lib/state.cjs +736 -736
  67. package/ez-agents/bin/lib/temp-file.cjs +239 -239
  68. package/ez-agents/bin/lib/template.cjs +223 -223
  69. package/ez-agents/bin/lib/test-file-lock.cjs +112 -112
  70. package/ez-agents/bin/lib/test-graceful.cjs +93 -93
  71. package/ez-agents/bin/lib/test-logger.cjs +60 -60
  72. package/ez-agents/bin/lib/test-safe-exec.cjs +38 -38
  73. package/ez-agents/bin/lib/test-safe-path.cjs +33 -33
  74. package/ez-agents/bin/lib/test-temp-file.cjs +125 -125
  75. package/ez-agents/bin/lib/tier-manager.cjs +428 -0
  76. package/ez-agents/bin/lib/timeout-exec.cjs +63 -63
  77. package/ez-agents/bin/lib/url-fetch.cjs +170 -0
  78. package/ez-agents/bin/lib/verify.cjs +15 -1
  79. package/ez-agents/references/checkpoints.md +776 -776
  80. package/ez-agents/references/continuation-format.md +249 -249
  81. package/ez-agents/references/metrics-schema.md +118 -0
  82. package/ez-agents/references/planning-config.md +140 -0
  83. package/ez-agents/references/questioning.md +162 -162
  84. package/ez-agents/references/tdd.md +263 -263
  85. package/ez-agents/references/tier-strategy.md +103 -0
  86. package/ez-agents/templates/bdd-feature.md +173 -0
  87. package/ez-agents/templates/codebase/concerns.md +310 -310
  88. package/ez-agents/templates/codebase/conventions.md +307 -307
  89. package/ez-agents/templates/codebase/integrations.md +280 -280
  90. package/ez-agents/templates/codebase/stack.md +186 -186
  91. package/ez-agents/templates/codebase/testing.md +480 -480
  92. package/ez-agents/templates/config.json +37 -37
  93. package/ez-agents/templates/continue-here.md +78 -78
  94. package/ez-agents/templates/discussion.md +68 -0
  95. package/ez-agents/templates/incident-runbook.md +205 -0
  96. package/ez-agents/templates/milestone-archive.md +123 -123
  97. package/ez-agents/templates/milestone.md +115 -115
  98. package/ez-agents/templates/release-checklist.md +133 -0
  99. package/ez-agents/templates/requirements.md +231 -231
  100. package/ez-agents/templates/research-project/ARCHITECTURE.md +204 -204
  101. package/ez-agents/templates/research-project/FEATURES.md +147 -147
  102. package/ez-agents/templates/research-project/PITFALLS.md +200 -200
  103. package/ez-agents/templates/research-project/STACK.md +120 -120
  104. package/ez-agents/templates/research-project/SUMMARY.md +170 -170
  105. package/ez-agents/templates/retrospective.md +54 -54
  106. package/ez-agents/templates/roadmap.md +202 -202
  107. package/ez-agents/templates/rollback-plan.md +201 -0
  108. package/ez-agents/templates/summary-minimal.md +41 -41
  109. package/ez-agents/templates/summary-standard.md +48 -48
  110. package/ez-agents/templates/summary.md +248 -248
  111. package/ez-agents/templates/user-setup.md +311 -311
  112. package/ez-agents/templates/verification-report.md +322 -322
  113. package/ez-agents/workflows/add-phase.md +112 -112
  114. package/ez-agents/workflows/add-tests.md +351 -351
  115. package/ez-agents/workflows/add-todo.md +158 -158
  116. package/ez-agents/workflows/arch-review.md +54 -0
  117. package/ez-agents/workflows/audit-milestone.md +332 -332
  118. package/ez-agents/workflows/autonomous.md +131 -30
  119. package/ez-agents/workflows/check-todos.md +177 -177
  120. package/ez-agents/workflows/cleanup.md +152 -152
  121. package/ez-agents/workflows/complete-milestone.md +766 -766
  122. package/ez-agents/workflows/diagnose-issues.md +219 -219
  123. package/ez-agents/workflows/discovery-phase.md +289 -289
  124. package/ez-agents/workflows/discuss-phase.md +762 -762
  125. package/ez-agents/workflows/execute-phase.md +513 -468
  126. package/ez-agents/workflows/execute-plan.md +483 -483
  127. package/ez-agents/workflows/export-session.md +255 -0
  128. package/ez-agents/workflows/gather-requirements.md +206 -0
  129. package/ez-agents/workflows/health.md +159 -159
  130. package/ez-agents/workflows/help.md +584 -492
  131. package/ez-agents/workflows/hotfix.md +291 -0
  132. package/ez-agents/workflows/import-session.md +303 -0
  133. package/ez-agents/workflows/insert-phase.md +130 -130
  134. package/ez-agents/workflows/list-phase-assumptions.md +178 -178
  135. package/ez-agents/workflows/map-codebase.md +316 -316
  136. package/ez-agents/workflows/new-milestone.md +339 -10
  137. package/ez-agents/workflows/new-project.md +293 -299
  138. package/ez-agents/workflows/node-repair.md +92 -92
  139. package/ez-agents/workflows/pause-work.md +122 -122
  140. package/ez-agents/workflows/plan-milestone-gaps.md +274 -274
  141. package/ez-agents/workflows/plan-phase.md +673 -651
  142. package/ez-agents/workflows/progress.md +372 -382
  143. package/ez-agents/workflows/quick.md +610 -610
  144. package/ez-agents/workflows/release.md +253 -0
  145. package/ez-agents/workflows/remove-phase.md +155 -155
  146. package/ez-agents/workflows/research-phase.md +74 -74
  147. package/ez-agents/workflows/resume-project.md +307 -307
  148. package/ez-agents/workflows/resume-session.md +215 -0
  149. package/ez-agents/workflows/set-profile.md +81 -81
  150. package/ez-agents/workflows/settings.md +242 -242
  151. package/ez-agents/workflows/standup.md +64 -0
  152. package/ez-agents/workflows/stats.md +57 -57
  153. package/ez-agents/workflows/transition.md +544 -544
  154. package/ez-agents/workflows/ui-phase.md +290 -290
  155. package/ez-agents/workflows/ui-review.md +157 -157
  156. package/ez-agents/workflows/update.md +320 -320
  157. package/ez-agents/workflows/validate-phase.md +167 -167
  158. package/ez-agents/workflows/verify-phase.md +243 -243
  159. package/ez-agents/workflows/verify-work.md +584 -584
  160. package/package.json +10 -4
  161. package/scripts/build-hooks.js +43 -43
  162. package/scripts/run-tests.cjs +29 -29
@@ -0,0 +1,458 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Discussion Synthesizer — Reads DISCUSSION.md, extracts consensus and blockers
5
+ *
6
+ * Parses the multi-agent DISCUSSION.md format to extract:
7
+ * - Hard blockers from any agent
8
+ * - Warnings and advisory notes
9
+ * - Consensus status (open | consensus-reached | needs-human)
10
+ * - Go/No-Go recommendation
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { withLock } = require('./file-lock.cjs');
18
+
19
+ // ─────────────────────────────────────────────
20
+ // Parser
21
+ // ─────────────────────────────────────────────
22
+
23
+ /**
24
+ * Parse DISCUSSION.md file
25
+ * @param {string} filePath
26
+ * @returns {{ frontmatter: object, sections: object[], consensus: object, blockers: string[], warnings: string[] }}
27
+ */
28
+ function parseDiscussion(filePath) {
29
+ if (!fs.existsSync(filePath)) {
30
+ return {
31
+ found: false,
32
+ filePath,
33
+ frontmatter: {},
34
+ sections: [],
35
+ consensus: { status: 'open', goNoGo: 'GO', rationale: 'No discussion file — proceeding' },
36
+ blockers: [],
37
+ warnings: []
38
+ };
39
+ }
40
+
41
+ const content = fs.readFileSync(filePath, 'utf8');
42
+ const lines = content.split('\n');
43
+
44
+ // Parse YAML frontmatter
45
+ const frontmatter = parseFrontmatter(content);
46
+
47
+ // Parse agent sections
48
+ const sections = parseAgentSections(lines);
49
+
50
+ // Extract blockers and warnings from all sections
51
+ const blockers = [];
52
+ const warnings = [];
53
+
54
+ for (const section of sections) {
55
+ const sectionBlockers = extractBlockers(section.content);
56
+ const sectionWarnings = extractWarnings(section.content);
57
+ blockers.push(...sectionBlockers.map(b => ({ agent: section.agent, text: b })));
58
+ warnings.push(...sectionWarnings.map(w => ({ agent: section.agent, text: w })));
59
+ }
60
+
61
+ // Parse consensus section
62
+ const consensus = parseConsensus(sections);
63
+
64
+ return {
65
+ found: true,
66
+ filePath,
67
+ frontmatter,
68
+ sections,
69
+ consensus,
70
+ blockers,
71
+ warnings,
72
+ hasBlockers: blockers.length > 0,
73
+ hasWarnings: warnings.length > 0
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Parse YAML frontmatter from discussion file
79
+ */
80
+ function parseFrontmatter(content) {
81
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
82
+ if (!match) return {};
83
+
84
+ const fm = {};
85
+ const lines = match[1].split('\n');
86
+ for (const line of lines) {
87
+ const colonIdx = line.indexOf(':');
88
+ if (colonIdx === -1) continue;
89
+ const key = line.slice(0, colonIdx).trim();
90
+ const value = line.slice(colonIdx + 1).trim();
91
+ // Remove quotes
92
+ fm[key] = value.replace(/^['"]|['"]$/g, '');
93
+ }
94
+ return fm;
95
+ }
96
+
97
+ /**
98
+ * Parse agent sections from DISCUSSION.md
99
+ * Returns array of { agent, heading, content }
100
+ */
101
+ function parseAgentSections(lines) {
102
+ const sections = [];
103
+ let currentSection = null;
104
+ let currentContent = [];
105
+
106
+ const agentHeadings = [
107
+ { pattern: /## Requirements Perspective/, agent: 'requirements' },
108
+ { pattern: /## Tech Lead Perspective/, agent: 'tech-lead' },
109
+ { pattern: /## Observer Perspective/, agent: 'observer' },
110
+ { pattern: /## Scrum Master Perspective/, agent: 'scrum-master' },
111
+ { pattern: /## Consensus/, agent: 'consensus' }
112
+ ];
113
+
114
+ for (let i = 0; i < lines.length; i++) {
115
+ const line = lines[i];
116
+
117
+ // Check if line starts a known section
118
+ const matchedHeading = agentHeadings.find(h => h.pattern.test(line));
119
+
120
+ if (matchedHeading) {
121
+ // Save previous section
122
+ if (currentSection) {
123
+ sections.push({
124
+ agent: currentSection.agent,
125
+ heading: currentSection.heading,
126
+ content: currentContent.join('\n').trim()
127
+ });
128
+ }
129
+ currentSection = { agent: matchedHeading.agent, heading: line };
130
+ currentContent = [];
131
+ } else if (currentSection) {
132
+ currentContent.push(line);
133
+ }
134
+ }
135
+
136
+ // Push last section
137
+ if (currentSection) {
138
+ sections.push({
139
+ agent: currentSection.agent,
140
+ heading: currentSection.heading,
141
+ content: currentContent.join('\n').trim()
142
+ });
143
+ }
144
+
145
+ return sections;
146
+ }
147
+
148
+ /**
149
+ * Extract blocker statements from section content
150
+ * Looks for BLOCKER markers used by agents
151
+ */
152
+ function extractBlockers(content) {
153
+ const blockers = [];
154
+ const lines = content.split('\n');
155
+
156
+ for (const line of lines) {
157
+ if (line.includes('🛑') || line.includes('BLOCKER') || line.match(/\*\*BLOCKER/i)) {
158
+ // Extract the description after the marker
159
+ const cleaned = line
160
+ .replace(/[🛑*]/g, '')
161
+ .replace(/BLOCKER\s*—?\s*/i, '')
162
+ .trim();
163
+ if (cleaned && cleaned.length > 3) {
164
+ blockers.push(cleaned);
165
+ }
166
+ }
167
+ }
168
+
169
+ return blockers;
170
+ }
171
+
172
+ /**
173
+ * Extract warning statements from section content
174
+ */
175
+ function extractWarnings(content) {
176
+ const warnings = [];
177
+ const lines = content.split('\n');
178
+
179
+ for (const line of lines) {
180
+ if (line.includes('⚠️') || line.match(/\*\*WARNING/i)) {
181
+ const cleaned = line
182
+ .replace(/[⚠️*]/g, '')
183
+ .replace(/WARNING\s*—?\s*/i, '')
184
+ .trim();
185
+ if (cleaned && cleaned.length > 3) {
186
+ warnings.push(cleaned);
187
+ }
188
+ }
189
+ }
190
+
191
+ return warnings;
192
+ }
193
+
194
+ /**
195
+ * Parse consensus section for Go/No-Go status
196
+ */
197
+ function parseConsensus(sections) {
198
+ const consensusSection = sections.find(s => s.agent === 'consensus');
199
+
200
+ if (!consensusSection) {
201
+ return { status: 'open', goNoGo: 'GO', rationale: 'No consensus section yet' };
202
+ }
203
+
204
+ const content = consensusSection.content;
205
+
206
+ // Extract Go/No-Go
207
+ let goNoGo = 'GO';
208
+ if (content.match(/NO-GO/i)) goNoGo = 'NO-GO';
209
+ else if (content.match(/HUMAN-NEEDED/i)) goNoGo = 'HUMAN-NEEDED';
210
+ else if (content.match(/^.*GO.*$/m)) goNoGo = 'GO';
211
+
212
+ // Extract status
213
+ let status = 'open';
214
+ if (content.match(/consensus-reached/i) || goNoGo !== 'open') status = 'consensus-reached';
215
+ if (goNoGo === 'HUMAN-NEEDED') status = 'needs-human';
216
+
217
+ // Extract rationale
218
+ const rationaleMatch = content.match(/### Rationale\n([^\n]+)/);
219
+ const rationale = rationaleMatch ? rationaleMatch[1].trim() : '';
220
+
221
+ return { status, goNoGo, rationale };
222
+ }
223
+
224
+ // ─────────────────────────────────────────────
225
+ // Synthesis
226
+ // ─────────────────────────────────────────────
227
+
228
+ /**
229
+ * Synthesize discussion into orchestrator-ready decision
230
+ * @param {string} discussionPath - Path to DISCUSSION.md
231
+ * @returns {{ proceed: boolean, reason: string, blockers: object[], warnings: object[], score: object }}
232
+ */
233
+ function synthesize(discussionPath) {
234
+ const discussion = parseDiscussion(discussionPath);
235
+
236
+ if (!discussion.found) {
237
+ return {
238
+ proceed: true,
239
+ reason: 'No discussion file — no pre-flight concerns',
240
+ blockers: [],
241
+ warnings: [],
242
+ agentsParticipated: []
243
+ };
244
+ }
245
+
246
+ const hasBlockers = discussion.blockers.length > 0;
247
+ const consensusGoNoGo = discussion.consensus.goNoGo;
248
+
249
+ // Determine whether to proceed
250
+ let proceed = true;
251
+ let reason = '';
252
+
253
+ if (hasBlockers || consensusGoNoGo === 'NO-GO') {
254
+ proceed = false;
255
+ reason = hasBlockers
256
+ ? `${discussion.blockers.length} blocker(s) must be resolved before execution`
257
+ : 'Consensus is NO-GO';
258
+ } else if (consensusGoNoGo === 'HUMAN-NEEDED') {
259
+ proceed = false;
260
+ reason = 'Human input required before proceeding';
261
+ } else {
262
+ proceed = true;
263
+ reason = discussion.warnings.length > 0
264
+ ? `${discussion.warnings.length} warning(s) — proceeding with awareness`
265
+ : 'No blockers found — proceeding';
266
+ }
267
+
268
+ const agentsParticipated = discussion.sections
269
+ .filter(s => s.agent !== 'consensus')
270
+ .filter(s => !s.content.includes('{Populated') && s.content.trim().length > 20)
271
+ .map(s => s.agent);
272
+
273
+ return {
274
+ proceed,
275
+ reason,
276
+ blockers: discussion.blockers,
277
+ warnings: discussion.warnings,
278
+ consensus: discussion.consensus,
279
+ agentsParticipated,
280
+ frontmatter: discussion.frontmatter
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Check if a discussion file needs updating (agents haven't written yet)
286
+ * @param {string} discussionPath
287
+ * @returns {{ needsObserver: boolean, needsTechLead: boolean, needsScrumMaster: boolean }}
288
+ */
289
+ function checkParticipation(discussionPath) {
290
+ const discussion = parseDiscussion(discussionPath);
291
+
292
+ if (!discussion.found) {
293
+ return { needsObserver: true, needsTechLead: true, needsScrumMaster: true };
294
+ }
295
+
296
+ const populated = (agent) => {
297
+ const section = discussion.sections.find(s => s.agent === agent);
298
+ return section && !section.content.includes('{Populated') && section.content.trim().length > 20;
299
+ };
300
+
301
+ return {
302
+ needsObserver: !populated('observer'),
303
+ needsTechLead: !populated('tech-lead'),
304
+ needsScrumMaster: !populated('scrum-master'),
305
+ needsRequirements: !populated('requirements')
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Update consensus section in DISCUSSION.md
311
+ * @param {string} discussionPath
312
+ * @param {object} consensusData - { goNoGo, blockers, warnings, rationale }
313
+ */
314
+ async function updateConsensus(discussionPath, consensusData) {
315
+ if (!fs.existsSync(discussionPath)) return false;
316
+ return withLock(discussionPath, async () => {
317
+ const content = fs.readFileSync(discussionPath, 'utf8');
318
+
319
+ const blockerList = consensusData.blockers.length > 0
320
+ ? consensusData.blockers.map(b => `- 🛑 ${b.agent}: ${b.text}`).join('\n')
321
+ : 'None';
322
+
323
+ const warningList = consensusData.warnings.length > 0
324
+ ? consensusData.warnings.slice(0, 5).map(w => `- ⚠️ ${w.agent}: ${w.text}`).join('\n')
325
+ : 'None';
326
+
327
+ const now = new Date().toISOString();
328
+ const status = consensusData.goNoGo === 'GO'
329
+ ? 'consensus-reached'
330
+ : consensusData.goNoGo === 'HUMAN-NEEDED' ? 'needs-human' : 'consensus-reached';
331
+
332
+ const consensusSection = `## Consensus
333
+
334
+ > *Synthesized by orchestrator from above perspectives*
335
+
336
+ **Status:** ${status}
337
+
338
+ ### Blockers
339
+ ${blockerList}
340
+
341
+ ### Key Warnings
342
+ ${warningList}
343
+
344
+ ### Go / No-Go
345
+ ${consensusData.goNoGo} — ${consensusData.rationale}
346
+
347
+ ### Rationale
348
+ ${consensusData.rationale}
349
+
350
+ ---
351
+
352
+ *Discussion opened: {timestamp}*
353
+ *Last updated: ${now}*`;
354
+
355
+ // Replace existing consensus section or append
356
+ let updated;
357
+ if (content.includes('## Consensus')) {
358
+ updated = content.replace(/## Consensus[\s\S]*$/, consensusSection);
359
+ } else {
360
+ updated = content + '\n\n' + consensusSection;
361
+ }
362
+
363
+ // Also update frontmatter status
364
+ updated = updated.replace(/^status: .*$/m, `status: ${status}`);
365
+
366
+ fs.writeFileSync(discussionPath, updated, 'utf8');
367
+ return true;
368
+ });
369
+ }
370
+
371
+ /**
372
+ * Format synthesis result as human-readable text
373
+ * @param {object} result - From synthesize()
374
+ * @returns {string}
375
+ */
376
+ function formatSynthesis(result) {
377
+ const lines = [];
378
+
379
+ lines.push(`## Pre-Flight Discussion Summary`);
380
+ lines.push(`**Decision:** ${result.proceed ? '✓ GO — proceed to execution' : '✗ ' + (result.consensus && result.consensus.goNoGo === 'HUMAN-NEEDED' ? 'HUMAN-NEEDED' : 'NO-GO')}`);
381
+ lines.push(`**Reason:** ${result.reason}`);
382
+ lines.push(`**Agents participated:** ${result.agentsParticipated.join(', ') || 'none'}`);
383
+
384
+ if (result.blockers.length > 0) {
385
+ lines.push('');
386
+ lines.push('### Blockers (must resolve)');
387
+ for (const b of result.blockers) {
388
+ lines.push(`- 🛑 **${b.agent}:** ${b.text}`);
389
+ }
390
+ }
391
+
392
+ if (result.warnings.length > 0) {
393
+ lines.push('');
394
+ lines.push('### Warnings (advisory)');
395
+ for (const w of result.warnings.slice(0, 5)) {
396
+ lines.push(`- ⚠️ **${w.agent}:** ${w.text}`);
397
+ }
398
+ }
399
+
400
+ return lines.join('\n');
401
+ }
402
+
403
+ // ─────────────────────────────────────────────
404
+ // CLI Interface
405
+ // ─────────────────────────────────────────────
406
+
407
+ if (require.main === module) {
408
+ const args = process.argv.slice(2);
409
+ const cmd = args[0];
410
+ const discussionPath = args[1];
411
+
412
+ if (!cmd) {
413
+ console.error('Usage: discussion-synthesizer.cjs <synthesize|check-participation|update-consensus> <path> [options]');
414
+ process.exit(1);
415
+ }
416
+
417
+ (async () => {
418
+ try {
419
+ if (cmd === 'synthesize') {
420
+ if (!discussionPath) { console.error('Path required'); process.exit(1); }
421
+ const result = synthesize(discussionPath);
422
+ if (args.includes('--json')) {
423
+ console.log(JSON.stringify(result, null, 2));
424
+ } else {
425
+ console.log(formatSynthesis(result));
426
+ process.exit(result.proceed ? 0 : 1);
427
+ }
428
+ } else if (cmd === 'check-participation') {
429
+ if (!discussionPath) { console.error('Path required'); process.exit(1); }
430
+ const result = checkParticipation(discussionPath);
431
+ console.log(JSON.stringify(result, null, 2));
432
+ } else if (cmd === 'update-consensus') {
433
+ if (!discussionPath) { console.error('Path required'); process.exit(1); }
434
+ const dataArg = args[2];
435
+ if (!dataArg) { console.error('Consensus data JSON required'); process.exit(1); }
436
+ const data = JSON.parse(dataArg);
437
+ const ok = await updateConsensus(discussionPath, data);
438
+ console.log(JSON.stringify({ updated: ok }));
439
+ } else {
440
+ console.error(`Unknown command: ${cmd}`);
441
+ process.exit(1);
442
+ }
443
+ } catch (err) {
444
+ console.error(`Error: ${err.message}`);
445
+ process.exit(1);
446
+ }
447
+ })();
448
+ }
449
+
450
+ module.exports = {
451
+ parseDiscussion,
452
+ synthesize,
453
+ checkParticipation,
454
+ updateConsensus,
455
+ formatSynthesis,
456
+ extractBlockers,
457
+ extractWarnings
458
+ };
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * File Access Service
5
+ *
6
+ * Provides file reading capabilities with glob pattern support.
7
+ * Uses micromatch for glob matching with support for negation and brace expansion.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const micromatch = require('micromatch');
13
+ const { FileAccessError } = require('./context-errors.cjs');
14
+
15
+ const MAX_FILE_COUNT = 1000;
16
+
17
+ class FileAccessService {
18
+ /**
19
+ * Create a new FileAccessService instance
20
+ * @param {string} cwd - Current working directory (defaults to process.cwd())
21
+ */
22
+ constructor(cwd) {
23
+ this.cwd = cwd || process.cwd();
24
+ }
25
+
26
+ /**
27
+ * Read files matching patterns
28
+ * @param {string|string[]} patterns - File patterns (glob or single path)
29
+ * @returns {Array<{path: string, content: string}>} - Array of file objects
30
+ * @throws {FileAccessError} - On file access errors
31
+ */
32
+ readFiles(patterns) {
33
+ const patternArray = Array.isArray(patterns) ? patterns : [patterns];
34
+
35
+ // Get all files recursively
36
+ const allFiles = this._getAllFiles(this.cwd);
37
+
38
+ // Convert paths to relative paths from cwd
39
+ const relativeFiles = allFiles.map(f => {
40
+ const relPath = path.relative(this.cwd, f);
41
+ // Convert to POSIX style paths for glob matching
42
+ return relPath.replace(/\\/g, '/');
43
+ });
44
+
45
+ // Filter with micromatch
46
+ const matchedFiles = micromatch.match(relativeFiles, patternArray, {
47
+ dot: false, // Don't match hidden files/directories by default
48
+ nocase: false
49
+ });
50
+
51
+ // Check max file count
52
+ if (matchedFiles.length > MAX_FILE_COUNT) {
53
+ throw new FileAccessError(
54
+ matchedFiles[0],
55
+ `Max file count exceeded: ${matchedFiles.length} > ${MAX_FILE_COUNT}`
56
+ );
57
+ }
58
+
59
+ // Read file contents
60
+ const results = matchedFiles.map(filePath => {
61
+ const fullPath = path.join(this.cwd, filePath);
62
+
63
+ if (!fs.existsSync(fullPath)) {
64
+ throw new FileAccessError(filePath, 'File not found');
65
+ }
66
+
67
+ try {
68
+ const content = fs.readFileSync(fullPath, 'utf-8');
69
+ return {
70
+ path: filePath,
71
+ content
72
+ };
73
+ } catch (err) {
74
+ if (err.code === 'EACCES') {
75
+ throw new FileAccessError(filePath, 'Permission denied');
76
+ }
77
+ throw new FileAccessError(filePath, err.message);
78
+ }
79
+ });
80
+
81
+ return results;
82
+ }
83
+
84
+ /**
85
+ * Get all files recursively from a directory
86
+ * @param {string} dir - Directory to scan
87
+ * @returns {string[]} - Array of file paths
88
+ * @private
89
+ */
90
+ _getAllFiles(dir) {
91
+ const files = [];
92
+
93
+ try {
94
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
95
+
96
+ for (const entry of entries) {
97
+ const fullPath = path.join(dir, entry.name);
98
+
99
+ // Skip hidden directories (starting with .)
100
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
101
+ const subFiles = this._getAllFiles(fullPath);
102
+ files.push(...subFiles);
103
+ } else if (entry.isFile()) {
104
+ files.push(fullPath);
105
+ }
106
+ }
107
+ } catch (err) {
108
+ // Ignore permission errors during directory traversal
109
+ if (err.code !== 'EACCES') {
110
+ throw err;
111
+ }
112
+ }
113
+
114
+ return files;
115
+ }
116
+
117
+ /**
118
+ * Normalize a file path (convert Windows paths to Unix style)
119
+ * @param {string} filePath - The path to normalize
120
+ * @returns {string} - Normalized path
121
+ * @throws {FileAccessError} - On path traversal attempts
122
+ */
123
+ normalizePath(filePath) {
124
+ // Convert Windows backslashes to forward slashes
125
+ const normalized = filePath.replace(/\\/g, '/');
126
+
127
+ // Check for path traversal attempts
128
+ if (normalized.includes('..')) {
129
+ throw new FileAccessError(filePath, 'Path traversal not allowed');
130
+ }
131
+
132
+ return normalized;
133
+ }
134
+
135
+ /**
136
+ * Validate a file path
137
+ * @param {string} filePath - The path to validate
138
+ * @returns {boolean} - True if valid
139
+ */
140
+ validatePath(filePath) {
141
+ // Reject paths with null bytes
142
+ if (filePath.includes('\x00')) {
143
+ return false;
144
+ }
145
+
146
+ // Resolve the path
147
+ const resolvedPath = path.resolve(this.cwd, filePath);
148
+
149
+ // Check if path is within cwd (prevent access outside project)
150
+ const normalizedCwd = path.resolve(this.cwd).replace(/\\/g, '/');
151
+ const normalizedResolved = resolvedPath.replace(/\\/g, '/');
152
+
153
+ // Path must be within or equal to cwd
154
+ if (!normalizedResolved.startsWith(normalizedCwd)) {
155
+ return false;
156
+ }
157
+
158
+ return true;
159
+ }
160
+
161
+ /**
162
+ * Check if a file exists
163
+ * @param {string} filePath - Path to check
164
+ * @returns {boolean} - True if file exists
165
+ */
166
+ fileExists(filePath) {
167
+ const fullPath = path.join(this.cwd, filePath);
168
+ return fs.existsSync(fullPath);
169
+ }
170
+
171
+ /**
172
+ * Read a single file
173
+ * @param {string} filePath - Path to the file
174
+ * @returns {{path: string, content: string}} - File object
175
+ * @throws {FileAccessError} - On file access errors
176
+ */
177
+ readFile(filePath) {
178
+ const results = this.readFiles([filePath]);
179
+ return results[0] || null;
180
+ }
181
+
182
+ /**
183
+ * Get file info (size, modified time, etc.)
184
+ * @param {string} filePath - Path to the file
185
+ * @returns {{path: string, size: number, mtime: Date}} - File info
186
+ * @throws {FileAccessError} - On file access errors
187
+ */
188
+ getFileInfo(filePath) {
189
+ const fullPath = path.join(this.cwd, filePath);
190
+
191
+ if (!fs.existsSync(fullPath)) {
192
+ throw new FileAccessError(filePath, 'File not found');
193
+ }
194
+
195
+ const stats = fs.statSync(fullPath);
196
+
197
+ return {
198
+ path: filePath,
199
+ size: stats.size,
200
+ mtime: stats.mtime,
201
+ isDirectory: stats.isDirectory(),
202
+ isFile: stats.isFile()
203
+ };
204
+ }
205
+ }
206
+
207
+ module.exports = FileAccessService;