agileflow 2.95.2 → 2.96.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 (81) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -6
  3. package/lib/api-routes.js +605 -0
  4. package/lib/api-server.js +260 -0
  5. package/lib/claude-cli-bridge.js +221 -0
  6. package/lib/dashboard-protocol.js +541 -0
  7. package/lib/dashboard-server.js +1601 -0
  8. package/lib/drivers/claude-driver.ts +310 -0
  9. package/lib/drivers/codex-driver.ts +454 -0
  10. package/lib/drivers/driver-manager.ts +158 -0
  11. package/lib/drivers/gemini-driver.ts +485 -0
  12. package/lib/drivers/index.ts +17 -0
  13. package/lib/flag-detection.js +350 -0
  14. package/lib/git-operations.js +267 -0
  15. package/lib/lock-file.js +144 -0
  16. package/lib/merge-operations.js +959 -0
  17. package/lib/protocol/driver.ts +360 -0
  18. package/lib/protocol/index.ts +12 -0
  19. package/lib/protocol/ir.ts +271 -0
  20. package/lib/session-display.js +330 -0
  21. package/lib/worktree-operations.js +221 -0
  22. package/package.json +2 -2
  23. package/scripts/agileflow-welcome.js +272 -24
  24. package/scripts/api-server-runner.js +177 -0
  25. package/scripts/archive-completed-stories.sh +22 -0
  26. package/scripts/automation-run-due.js +126 -0
  27. package/scripts/backfill-ideation-status.js +124 -0
  28. package/scripts/claude-tmux.sh +62 -1
  29. package/scripts/context-loader.js +292 -0
  30. package/scripts/dashboard-serve.js +323 -0
  31. package/scripts/lib/automation-registry.js +544 -0
  32. package/scripts/lib/automation-runner.js +476 -0
  33. package/scripts/lib/concurrency-limiter.js +513 -0
  34. package/scripts/lib/configure-features.js +46 -0
  35. package/scripts/lib/context-formatter.js +61 -0
  36. package/scripts/lib/damage-control-utils.js +29 -4
  37. package/scripts/lib/hook-metrics.js +324 -0
  38. package/scripts/lib/ideation-index.js +1196 -0
  39. package/scripts/lib/process-cleanup.js +359 -0
  40. package/scripts/lib/quality-gates.js +574 -0
  41. package/scripts/lib/status-task-bridge.js +522 -0
  42. package/scripts/lib/sync-ideation-status.js +292 -0
  43. package/scripts/lib/task-registry-cache.js +490 -0
  44. package/scripts/lib/task-registry.js +1181 -0
  45. package/scripts/migrate-ideation-index.js +515 -0
  46. package/scripts/precompact-context.sh +104 -0
  47. package/scripts/ralph-loop.js +2 -2
  48. package/scripts/session-manager.js +363 -2770
  49. package/scripts/spawn-parallel.js +45 -9
  50. package/src/core/agents/api-validator.md +180 -0
  51. package/src/core/agents/api.md +2 -0
  52. package/src/core/agents/code-reviewer.md +289 -0
  53. package/src/core/agents/configuration/damage-control.md +17 -0
  54. package/src/core/agents/database.md +2 -0
  55. package/src/core/agents/error-analyzer.md +203 -0
  56. package/src/core/agents/logic-analyzer-edge.md +171 -0
  57. package/src/core/agents/logic-analyzer-flow.md +254 -0
  58. package/src/core/agents/logic-analyzer-invariant.md +207 -0
  59. package/src/core/agents/logic-analyzer-race.md +267 -0
  60. package/src/core/agents/logic-analyzer-type.md +218 -0
  61. package/src/core/agents/logic-consensus.md +256 -0
  62. package/src/core/agents/orchestrator.md +89 -1
  63. package/src/core/agents/schema-validator.md +451 -0
  64. package/src/core/agents/team-coordinator.md +328 -0
  65. package/src/core/agents/ui-validator.md +328 -0
  66. package/src/core/agents/ui.md +2 -0
  67. package/src/core/commands/api.md +267 -0
  68. package/src/core/commands/automate.md +415 -0
  69. package/src/core/commands/babysit.md +290 -9
  70. package/src/core/commands/ideate/history.md +403 -0
  71. package/src/core/commands/{ideate.md → ideate/new.md} +244 -34
  72. package/src/core/commands/logic/audit.md +368 -0
  73. package/src/core/commands/roadmap/analyze.md +1 -1
  74. package/src/core/experts/documentation/expertise.yaml +29 -2
  75. package/src/core/templates/CONTEXT.md.example +49 -0
  76. package/src/core/templates/claude-settings.advanced.example.json +4 -0
  77. package/tools/cli/commands/serve.js +456 -0
  78. package/tools/cli/installers/core/installer.js +7 -2
  79. package/tools/cli/installers/ide/claude-code.js +85 -0
  80. package/tools/cli/lib/content-injector.js +27 -1
  81. package/tools/cli/lib/ui.js +26 -57
@@ -0,0 +1,324 @@
1
+ /**
2
+ * hook-metrics.js - Hook timing and performance metrics utility
3
+ *
4
+ * Provides utilities for recording hook execution metrics to session-state.json.
5
+ * This enables observability into hook performance without changing tech stack.
6
+ *
7
+ * Usage:
8
+ * const { startHookTimer, recordHookMetrics, getHookMetrics } = require('./lib/hook-metrics');
9
+ *
10
+ * // At start of hook
11
+ * const timer = startHookTimer('SessionStart', 'welcome');
12
+ *
13
+ * // ... do hook work ...
14
+ *
15
+ * // At end of hook (success)
16
+ * recordHookMetrics(timer, 'success');
17
+ *
18
+ * // Or on failure
19
+ * recordHookMetrics(timer, 'error', 'Parse error');
20
+ *
21
+ * Metrics structure in session-state.json:
22
+ * {
23
+ * "hook_metrics": {
24
+ * "last_updated": "2026-02-02T12:00:00.000Z",
25
+ * "session_total_ms": 565,
26
+ * "hooks": {
27
+ * "SessionStart": {
28
+ * "welcome": { "duration_ms": 245, "status": "success", "at": "..." },
29
+ * "archive": { "duration_ms": 120, "status": "success", "at": "..." }
30
+ * },
31
+ * "PreToolUse": {
32
+ * "damage_control_bash": { "duration_ms": 50, "status": "success", "at": "..." }
33
+ * }
34
+ * }
35
+ * }
36
+ * }
37
+ */
38
+
39
+ const fs = require('fs');
40
+ const path = require('path');
41
+
42
+ // Get paths module if available, otherwise use defaults
43
+ let getSessionStatePath;
44
+ try {
45
+ ({ getSessionStatePath } = require('./paths'));
46
+ } catch (e) {
47
+ // Fallback: find session-state.json manually
48
+ getSessionStatePath = rootDir => {
49
+ const possiblePaths = [
50
+ path.join(rootDir, 'docs', '09-agents', 'session-state.json'),
51
+ path.join(rootDir, '.agileflow', 'session-state.json'),
52
+ ];
53
+ for (const p of possiblePaths) {
54
+ if (fs.existsSync(p)) return p;
55
+ }
56
+ return possiblePaths[0]; // Default to first path
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Find the project root directory
62
+ * @returns {string} Project root path
63
+ */
64
+ function findProjectRoot() {
65
+ let dir = process.cwd();
66
+ while (dir !== '/') {
67
+ if (fs.existsSync(path.join(dir, '.agileflow'))) {
68
+ return dir;
69
+ }
70
+ if (fs.existsSync(path.join(dir, 'docs', '09-agents'))) {
71
+ return dir;
72
+ }
73
+ dir = path.dirname(dir);
74
+ }
75
+ return process.cwd();
76
+ }
77
+
78
+ /**
79
+ * Start a timer for hook execution
80
+ *
81
+ * @param {string} hookEvent - Hook event type (SessionStart, PreToolUse, PreCompact, Stop)
82
+ * @param {string} hookName - Name of the specific hook (welcome, archive, damage_control_bash)
83
+ * @returns {object} Timer object with start time and metadata
84
+ */
85
+ function startHookTimer(hookEvent, hookName) {
86
+ return {
87
+ hookEvent,
88
+ hookName,
89
+ startTime: Date.now(),
90
+ startHrTime: process.hrtime.bigint(),
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Record hook execution metrics to session-state.json
96
+ *
97
+ * @param {object} timer - Timer object from startHookTimer
98
+ * @param {string} status - Execution status ('success', 'error', 'blocked', 'timeout')
99
+ * @param {string} [errorMessage] - Optional error message if status is 'error'
100
+ * @param {object} [options] - Additional options
101
+ * @param {string} [options.rootDir] - Project root directory (auto-detected if not provided)
102
+ * @returns {object} Result with { ok, duration_ms, error? }
103
+ */
104
+ function recordHookMetrics(timer, status, errorMessage = null, options = {}) {
105
+ const result = {
106
+ ok: false,
107
+ duration_ms: 0,
108
+ };
109
+
110
+ try {
111
+ // Calculate duration
112
+ const endTime = Date.now();
113
+ const durationMs = endTime - timer.startTime;
114
+ result.duration_ms = durationMs;
115
+
116
+ // Find project root and session state file
117
+ const rootDir = options.rootDir || findProjectRoot();
118
+ const sessionStatePath = getSessionStatePath(rootDir);
119
+
120
+ // Ensure directory exists
121
+ const dir = path.dirname(sessionStatePath);
122
+ if (!fs.existsSync(dir)) {
123
+ fs.mkdirSync(dir, { recursive: true });
124
+ }
125
+
126
+ // Read existing session state
127
+ let state = {};
128
+ if (fs.existsSync(sessionStatePath)) {
129
+ try {
130
+ state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
131
+ } catch (e) {
132
+ // Invalid JSON - start fresh
133
+ state = {};
134
+ }
135
+ }
136
+
137
+ // Initialize hook_metrics if not present
138
+ if (!state.hook_metrics) {
139
+ state.hook_metrics = {
140
+ last_updated: null,
141
+ session_total_ms: 0,
142
+ hooks: {},
143
+ };
144
+ }
145
+
146
+ // Initialize event type if not present
147
+ if (!state.hook_metrics.hooks[timer.hookEvent]) {
148
+ state.hook_metrics.hooks[timer.hookEvent] = {};
149
+ }
150
+
151
+ // Record the metric
152
+ const metric = {
153
+ duration_ms: durationMs,
154
+ status,
155
+ at: new Date().toISOString(),
156
+ };
157
+
158
+ if (errorMessage) {
159
+ metric.error = errorMessage;
160
+ }
161
+
162
+ state.hook_metrics.hooks[timer.hookEvent][timer.hookName] = metric;
163
+ state.hook_metrics.last_updated = new Date().toISOString();
164
+
165
+ // Recalculate session total
166
+ let total = 0;
167
+ for (const eventHooks of Object.values(state.hook_metrics.hooks)) {
168
+ for (const hookMetric of Object.values(eventHooks)) {
169
+ total += hookMetric.duration_ms || 0;
170
+ }
171
+ }
172
+ state.hook_metrics.session_total_ms = total;
173
+
174
+ // Write back
175
+ fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
176
+
177
+ result.ok = true;
178
+ } catch (e) {
179
+ result.error = e.message;
180
+ // Fail silently - metrics should never break hook execution
181
+ }
182
+
183
+ return result;
184
+ }
185
+
186
+ /**
187
+ * Get current hook metrics from session-state.json
188
+ *
189
+ * @param {object} [options] - Options
190
+ * @param {string} [options.rootDir] - Project root directory (auto-detected if not provided)
191
+ * @returns {object} Hook metrics or empty object if not found
192
+ */
193
+ function getHookMetrics(options = {}) {
194
+ try {
195
+ const rootDir = options.rootDir || findProjectRoot();
196
+ const sessionStatePath = getSessionStatePath(rootDir);
197
+
198
+ if (!fs.existsSync(sessionStatePath)) {
199
+ return { hooks: {}, session_total_ms: 0 };
200
+ }
201
+
202
+ const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
203
+ return state.hook_metrics || { hooks: {}, session_total_ms: 0 };
204
+ } catch (e) {
205
+ return { hooks: {}, session_total_ms: 0, error: e.message };
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Clear hook metrics (useful for testing or session reset)
211
+ *
212
+ * @param {object} [options] - Options
213
+ * @param {string} [options.rootDir] - Project root directory
214
+ * @returns {object} Result with { ok, error? }
215
+ */
216
+ function clearHookMetrics(options = {}) {
217
+ try {
218
+ const rootDir = options.rootDir || findProjectRoot();
219
+ const sessionStatePath = getSessionStatePath(rootDir);
220
+
221
+ if (!fs.existsSync(sessionStatePath)) {
222
+ return { ok: true };
223
+ }
224
+
225
+ const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
226
+ state.hook_metrics = {
227
+ last_updated: new Date().toISOString(),
228
+ session_total_ms: 0,
229
+ hooks: {},
230
+ };
231
+
232
+ fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
233
+ return { ok: true };
234
+ } catch (e) {
235
+ return { ok: false, error: e.message };
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Format hook metrics for display
241
+ *
242
+ * @param {object} metrics - Metrics from getHookMetrics()
243
+ * @returns {string} Formatted string for display
244
+ */
245
+ function formatHookMetrics(metrics) {
246
+ if (!metrics || !metrics.hooks) {
247
+ return 'No hook metrics recorded';
248
+ }
249
+
250
+ const lines = [];
251
+ lines.push(`Hook Metrics (total: ${metrics.session_total_ms}ms)`);
252
+ lines.push('─'.repeat(50));
253
+
254
+ for (const [event, hooks] of Object.entries(metrics.hooks)) {
255
+ lines.push(` ${event}:`);
256
+ for (const [name, data] of Object.entries(hooks)) {
257
+ const status = data.status === 'success' ? '✓' : data.status === 'error' ? '✗' : '?';
258
+ const error = data.error ? ` (${data.error})` : '';
259
+ lines.push(` ${status} ${name}: ${data.duration_ms}ms${error}`);
260
+ }
261
+ }
262
+
263
+ if (metrics.last_updated) {
264
+ lines.push('');
265
+ lines.push(`Last updated: ${metrics.last_updated}`);
266
+ }
267
+
268
+ return lines.join('\n');
269
+ }
270
+
271
+ /**
272
+ * Wrapper to time an async function and record metrics
273
+ *
274
+ * @param {string} hookEvent - Hook event type
275
+ * @param {string} hookName - Hook name
276
+ * @param {function} fn - Async function to time
277
+ * @param {object} [options] - Options passed to recordHookMetrics
278
+ * @returns {Promise<any>} Result of the function
279
+ */
280
+ async function withHookMetrics(hookEvent, hookName, fn, options = {}) {
281
+ const timer = startHookTimer(hookEvent, hookName);
282
+
283
+ try {
284
+ const result = await fn();
285
+ recordHookMetrics(timer, 'success', null, options);
286
+ return result;
287
+ } catch (e) {
288
+ recordHookMetrics(timer, 'error', e.message, options);
289
+ throw e;
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Wrapper to time a sync function and record metrics
295
+ *
296
+ * @param {string} hookEvent - Hook event type
297
+ * @param {string} hookName - Hook name
298
+ * @param {function} fn - Sync function to time
299
+ * @param {object} [options] - Options passed to recordHookMetrics
300
+ * @returns {any} Result of the function
301
+ */
302
+ function withHookMetricsSync(hookEvent, hookName, fn, options = {}) {
303
+ const timer = startHookTimer(hookEvent, hookName);
304
+
305
+ try {
306
+ const result = fn();
307
+ recordHookMetrics(timer, 'success', null, options);
308
+ return result;
309
+ } catch (e) {
310
+ recordHookMetrics(timer, 'error', e.message, options);
311
+ throw e;
312
+ }
313
+ }
314
+
315
+ module.exports = {
316
+ startHookTimer,
317
+ recordHookMetrics,
318
+ getHookMetrics,
319
+ clearHookMetrics,
320
+ formatHookMetrics,
321
+ withHookMetrics,
322
+ withHookMetricsSync,
323
+ findProjectRoot,
324
+ };