agileflow 3.1.0 → 3.2.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 (101) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +57 -85
  3. package/lib/dashboard-automations.js +130 -0
  4. package/lib/dashboard-git.js +254 -0
  5. package/lib/dashboard-inbox.js +64 -0
  6. package/lib/dashboard-protocol.js +1 -0
  7. package/lib/dashboard-server.js +114 -924
  8. package/lib/dashboard-session.js +136 -0
  9. package/lib/dashboard-status.js +72 -0
  10. package/lib/dashboard-terminal.js +354 -0
  11. package/lib/dashboard-websocket.js +88 -0
  12. package/lib/drivers/codex-driver.ts +4 -4
  13. package/lib/logger.js +106 -0
  14. package/package.json +4 -2
  15. package/scripts/agileflow-configure.js +2 -2
  16. package/scripts/agileflow-welcome.js +409 -434
  17. package/scripts/claude-tmux.sh +80 -2
  18. package/scripts/context-loader.js +4 -9
  19. package/scripts/lib/command-prereqs.js +280 -0
  20. package/scripts/lib/configure-detect.js +92 -2
  21. package/scripts/lib/configure-features.js +295 -1
  22. package/scripts/lib/context-formatter.js +468 -233
  23. package/scripts/lib/context-loader.js +27 -15
  24. package/scripts/lib/damage-control-utils.js +8 -1
  25. package/scripts/lib/feature-catalog.js +321 -0
  26. package/scripts/lib/portable-tasks-cli.js +274 -0
  27. package/scripts/lib/portable-tasks.js +479 -0
  28. package/scripts/lib/signal-detectors.js +1 -1
  29. package/scripts/lib/team-events.js +86 -1
  30. package/scripts/obtain-context.js +28 -4
  31. package/scripts/smart-detect.js +17 -0
  32. package/scripts/strip-ai-attribution.js +63 -0
  33. package/scripts/team-manager.js +7 -2
  34. package/scripts/welcome-deferred.js +437 -0
  35. package/src/core/agents/perf-analyzer-assets.md +174 -0
  36. package/src/core/agents/perf-analyzer-bundle.md +165 -0
  37. package/src/core/agents/perf-analyzer-caching.md +160 -0
  38. package/src/core/agents/perf-analyzer-compute.md +165 -0
  39. package/src/core/agents/perf-analyzer-memory.md +182 -0
  40. package/src/core/agents/perf-analyzer-network.md +157 -0
  41. package/src/core/agents/perf-analyzer-queries.md +155 -0
  42. package/src/core/agents/perf-analyzer-rendering.md +156 -0
  43. package/src/core/agents/perf-consensus.md +280 -0
  44. package/src/core/agents/security-analyzer-api.md +199 -0
  45. package/src/core/agents/security-analyzer-auth.md +160 -0
  46. package/src/core/agents/security-analyzer-authz.md +168 -0
  47. package/src/core/agents/security-analyzer-deps.md +147 -0
  48. package/src/core/agents/security-analyzer-infra.md +176 -0
  49. package/src/core/agents/security-analyzer-injection.md +148 -0
  50. package/src/core/agents/security-analyzer-input.md +191 -0
  51. package/src/core/agents/security-analyzer-secrets.md +175 -0
  52. package/src/core/agents/security-consensus.md +276 -0
  53. package/src/core/agents/test-analyzer-assertions.md +181 -0
  54. package/src/core/agents/test-analyzer-coverage.md +183 -0
  55. package/src/core/agents/test-analyzer-fragility.md +185 -0
  56. package/src/core/agents/test-analyzer-integration.md +155 -0
  57. package/src/core/agents/test-analyzer-maintenance.md +173 -0
  58. package/src/core/agents/test-analyzer-mocking.md +178 -0
  59. package/src/core/agents/test-analyzer-patterns.md +189 -0
  60. package/src/core/agents/test-analyzer-structure.md +177 -0
  61. package/src/core/agents/test-consensus.md +294 -0
  62. package/src/core/commands/{legal/audit.md → audit/legal.md} +13 -13
  63. package/src/core/commands/{logic/audit.md → audit/logic.md} +12 -12
  64. package/src/core/commands/audit/performance.md +443 -0
  65. package/src/core/commands/audit/security.md +443 -0
  66. package/src/core/commands/audit/test.md +442 -0
  67. package/src/core/commands/babysit.md +505 -463
  68. package/src/core/commands/configure.md +8 -8
  69. package/src/core/commands/research/ask.md +42 -9
  70. package/src/core/commands/research/import.md +14 -8
  71. package/src/core/commands/research/list.md +17 -16
  72. package/src/core/commands/research/synthesize.md +8 -8
  73. package/src/core/commands/research/view.md +28 -4
  74. package/src/core/commands/whats-new.md +2 -2
  75. package/src/core/experts/devops/expertise.yaml +13 -2
  76. package/src/core/experts/documentation/expertise.yaml +26 -4
  77. package/src/core/profiles/COMPARISON.md +170 -0
  78. package/src/core/profiles/README.md +178 -0
  79. package/src/core/profiles/claude-code.yaml +111 -0
  80. package/src/core/profiles/codex.yaml +103 -0
  81. package/src/core/profiles/cursor.yaml +134 -0
  82. package/src/core/profiles/examples.js +250 -0
  83. package/src/core/profiles/loader.js +235 -0
  84. package/src/core/profiles/windsurf.yaml +159 -0
  85. package/src/core/teams/logic-audit.json +6 -0
  86. package/src/core/teams/perf-audit.json +71 -0
  87. package/src/core/teams/security-audit.json +71 -0
  88. package/src/core/teams/test-audit.json +71 -0
  89. package/src/core/templates/command-prerequisites.yaml +169 -0
  90. package/src/core/templates/damage-control-patterns.yaml +9 -0
  91. package/tools/cli/installers/ide/_base-ide.js +33 -3
  92. package/tools/cli/installers/ide/claude-code.js +2 -69
  93. package/tools/cli/installers/ide/codex.js +9 -9
  94. package/tools/cli/installers/ide/cursor.js +165 -4
  95. package/tools/cli/installers/ide/windsurf.js +237 -6
  96. package/tools/cli/lib/content-transformer.js +234 -9
  97. package/tools/cli/lib/docs-setup.js +1 -1
  98. package/tools/cli/lib/ide-generator.js +357 -0
  99. package/tools/cli/lib/ide-registry.js +2 -2
  100. package/scripts/tmux-task-name.sh +0 -105
  101. package/scripts/tmux-task-watcher.sh +0 -344
@@ -0,0 +1,479 @@
1
+ /**
2
+ * portable-tasks.js - File-based task tracking for all IDEs
3
+ *
4
+ * Provides a portable, markdown-based task tracking system that works across
5
+ * all IDEs (Claude Code, Cursor, Windsurf, Codex). Unlike Claude Code's native
6
+ * TaskCreate/TaskUpdate tools, this system stores tasks in .agileflow/tasks.md
7
+ * where ANY IDE's AI can read/write.
8
+ *
9
+ * File Format (.agileflow/tasks.md):
10
+ * ```markdown
11
+ * # AgileFlow Tasks
12
+ *
13
+ * > Auto-managed task list. Edit carefully - format matters.
14
+ * > Last updated: 2026-02-20T15:30:00Z
15
+ *
16
+ * ## Active Tasks
17
+ *
18
+ * ### T-001: Task title [in_progress]
19
+ * - **Owner**: AG-API
20
+ * - **Created**: 2026-02-20
21
+ * - **Story**: US-0042
22
+ * - **Description**: Task details here
23
+ *
24
+ * ## Completed Tasks
25
+ *
26
+ * ### T-002: Completed task [completed]
27
+ * - **Owner**: AG-CI
28
+ * - **Created**: 2026-02-20
29
+ * - **Completed**: 2026-02-21
30
+ * ```
31
+ *
32
+ * Status values: pending, in_progress, completed, blocked
33
+ */
34
+
35
+ const fs = require('fs');
36
+ const path = require('path');
37
+
38
+ const AGILEFLOW_DIR = '.agileflow';
39
+ const TASKS_FILE = 'tasks.md';
40
+ const TASKS_PATH = path.join(AGILEFLOW_DIR, TASKS_FILE);
41
+
42
+ const STATUS_ACTIVE = ['pending', 'in_progress', 'blocked'];
43
+ const STATUS_COMPLETED = ['completed'];
44
+ const VALID_STATUSES = ['pending', 'in_progress', 'completed', 'blocked'];
45
+
46
+ /**
47
+ * Parse the tasks markdown file into structured data
48
+ * @param {string} content - Raw markdown file content
49
+ * @returns {Object} { activeTasks: [], completedTasks: [] } where each task is { id, title, status, owner, created, completed, story, blockedBy, description }
50
+ */
51
+ function parseTasksFile(content) {
52
+ const activeTasks = [];
53
+ const completedTasks = [];
54
+
55
+ if (!content || content.trim() === '') {
56
+ return { activeTasks, completedTasks };
57
+ }
58
+
59
+ const lines = content.split('\n');
60
+ let currentSection = null; // 'active' or 'completed'
61
+ let currentTask = null;
62
+
63
+ for (let i = 0; i < lines.length; i++) {
64
+ const line = lines[i];
65
+
66
+ // Detect section headers (must be before generic # check)
67
+ if (line.startsWith('## Active Tasks')) {
68
+ currentSection = 'active';
69
+ continue;
70
+ }
71
+ if (line.startsWith('## Completed Tasks')) {
72
+ currentSection = 'completed';
73
+ continue;
74
+ }
75
+
76
+ // Parse task headers: ### T-001: Title [status] (must be before generic # check)
77
+ const taskHeaderMatch = line.match(/^### (T-\d+):\s+(.+?)\s+\[(\w+)\]$/);
78
+ if (taskHeaderMatch) {
79
+ // Save previous task to appropriate list based on its status
80
+ if (currentTask) {
81
+ if (currentTask.status === 'completed') {
82
+ completedTasks.push(currentTask);
83
+ } else {
84
+ // All non-completed statuses go to active (pending, in_progress, blocked)
85
+ activeTasks.push(currentTask);
86
+ }
87
+ }
88
+
89
+ // Start new task
90
+ currentTask = {
91
+ id: taskHeaderMatch[1],
92
+ title: taskHeaderMatch[2].trim(),
93
+ status: taskHeaderMatch[3],
94
+ owner: null,
95
+ created: null,
96
+ completed: null,
97
+ story: null,
98
+ blockedBy: null,
99
+ description: null,
100
+ };
101
+ continue;
102
+ }
103
+
104
+ // Skip non-task lines (after checking for task headers and section headers)
105
+ if (!line.trim() || line.startsWith('>') || line.startsWith('#')) {
106
+ continue;
107
+ }
108
+
109
+ // Parse task fields: - **Field**: Value or - **Field name**: Value
110
+ if (currentTask && line.startsWith('- ')) {
111
+ const fieldMatch = line.match(/^- \*\*([^*]+)\*\*:\s+(.+)$/);
112
+ if (fieldMatch) {
113
+ const fieldName = fieldMatch[1].toLowerCase().trim();
114
+ const fieldValue = fieldMatch[2].trim();
115
+
116
+ switch (fieldName) {
117
+ case 'owner':
118
+ currentTask.owner = fieldValue;
119
+ break;
120
+ case 'created':
121
+ currentTask.created = fieldValue;
122
+ break;
123
+ case 'completed':
124
+ currentTask.completed = fieldValue;
125
+ break;
126
+ case 'story':
127
+ currentTask.story = fieldValue;
128
+ break;
129
+ case 'blockedby':
130
+ case 'blocked by':
131
+ currentTask.blockedBy = fieldValue;
132
+ break;
133
+ case 'description':
134
+ currentTask.description = fieldValue;
135
+ break;
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ // Save last task based on its status
142
+ if (currentTask) {
143
+ if (currentTask.status === 'completed') {
144
+ completedTasks.push(currentTask);
145
+ } else {
146
+ activeTasks.push(currentTask);
147
+ }
148
+ }
149
+
150
+ return { activeTasks, completedTasks };
151
+ }
152
+
153
+ /**
154
+ * Convert structured tasks back to markdown format
155
+ * @param {Array} activeTasks - Active task objects
156
+ * @param {Array} completedTasks - Completed task objects
157
+ * @returns {string} Formatted markdown content
158
+ */
159
+ /**
160
+ * Sanitize a string value to prevent markdown injection.
161
+ * Strips newlines and markdown heading markers that could create fake entries.
162
+ * @param {string} value - Raw string value
163
+ * @returns {string} Sanitized value safe for single-line markdown fields
164
+ */
165
+ function sanitizeField(value) {
166
+ if (!value || typeof value !== 'string') return value;
167
+ return value
168
+ .replace(/[\n\r]/g, ' ')
169
+ .replace(/#{2,}/g, '')
170
+ .trim();
171
+ }
172
+
173
+ function formatTasksFile(activeTasks, completedTasks) {
174
+ const now = new Date().toISOString();
175
+ let content = `# AgileFlow Tasks
176
+
177
+ > Auto-managed task list. Edit carefully - format matters.
178
+ > Last updated: ${now}
179
+
180
+ `;
181
+
182
+ // Active tasks section
183
+ if (activeTasks.length > 0) {
184
+ content += '## Active Tasks\n\n';
185
+ for (const task of activeTasks) {
186
+ content += `### ${task.id}: ${sanitizeField(task.title)} [${task.status}]\n`;
187
+ if (task.owner) content += `- **Owner**: ${sanitizeField(task.owner)}\n`;
188
+ if (task.created) content += `- **Created**: ${task.created}\n`;
189
+ if (task.story) content += `- **Story**: ${sanitizeField(task.story)}\n`;
190
+ if (task.blockedBy) content += `- **Blocked by**: ${sanitizeField(task.blockedBy)}\n`;
191
+ if (task.description) content += `- **Description**: ${sanitizeField(task.description)}\n`;
192
+ content += '\n';
193
+ }
194
+ }
195
+
196
+ // Completed tasks section
197
+ if (completedTasks.length > 0) {
198
+ content += '## Completed Tasks\n\n';
199
+ for (const task of completedTasks) {
200
+ content += `### ${task.id}: ${sanitizeField(task.title)} [${task.status}]\n`;
201
+ if (task.owner) content += `- **Owner**: ${sanitizeField(task.owner)}\n`;
202
+ if (task.created) content += `- **Created**: ${task.created}\n`;
203
+ if (task.completed) content += `- **Completed**: ${task.completed}\n`;
204
+ if (task.story) content += `- **Story**: ${sanitizeField(task.story)}\n`;
205
+ if (task.description) content += `- **Description**: ${sanitizeField(task.description)}\n`;
206
+ content += '\n';
207
+ }
208
+ }
209
+
210
+ return content;
211
+ }
212
+
213
+ /**
214
+ * Load and parse tasks from .agileflow/tasks.md
215
+ * @param {string} projectDir - Project directory (where .agileflow/ is located)
216
+ * @returns {Object} { activeTasks: [], completedTasks: [] }
217
+ */
218
+ function loadTasks(projectDir) {
219
+ const tasksPath = path.join(projectDir, TASKS_PATH);
220
+
221
+ try {
222
+ if (!fs.existsSync(tasksPath)) {
223
+ return { activeTasks: [], completedTasks: [] };
224
+ }
225
+
226
+ const content = fs.readFileSync(tasksPath, 'utf8');
227
+ return parseTasksFile(content);
228
+ } catch (e) {
229
+ // Return empty if read fails
230
+ return { activeTasks: [], completedTasks: [] };
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Save tasks back to .agileflow/tasks.md
236
+ * @param {string} projectDir - Project directory
237
+ * @param {Object} tasksData - { activeTasks: [], completedTasks: [] }
238
+ * @returns {boolean} True if successful
239
+ */
240
+ function saveTasks(projectDir, tasksData) {
241
+ try {
242
+ const agileflowDir = path.join(projectDir, AGILEFLOW_DIR);
243
+ const tasksPath = path.join(projectDir, TASKS_PATH);
244
+
245
+ // Ensure .agileflow directory exists
246
+ if (!fs.existsSync(agileflowDir)) {
247
+ fs.mkdirSync(agileflowDir, { recursive: true });
248
+ }
249
+
250
+ const content = formatTasksFile(tasksData.activeTasks, tasksData.completedTasks);
251
+ fs.writeFileSync(tasksPath, content, 'utf8');
252
+ return true;
253
+ } catch (e) {
254
+ return false;
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Get the next sequential task ID
260
+ * @param {Array} allTasks - Array of all tasks (active + completed)
261
+ * @returns {string} Next ID like T-001, T-002, etc.
262
+ */
263
+ function getNextId(allTasks) {
264
+ if (!allTasks || allTasks.length === 0) {
265
+ return 'T-001';
266
+ }
267
+
268
+ // Extract numeric parts from task IDs
269
+ const numbers = allTasks.map(t => parseInt(t.id.replace('T-', ''), 10)).filter(n => !isNaN(n));
270
+
271
+ if (numbers.length === 0) {
272
+ return 'T-001';
273
+ }
274
+
275
+ const maxNum = Math.max(...numbers);
276
+ return `T-${String(maxNum + 1).padStart(3, '0')}`;
277
+ }
278
+
279
+ /**
280
+ * Add a new task
281
+ * @param {string} projectDir - Project directory
282
+ * @param {Object} task - { subject, description, status, owner, story, blockedBy }
283
+ * @returns {Object} { ok: boolean, taskId?: string, error?: string }
284
+ */
285
+ function addTask(projectDir, task) {
286
+ try {
287
+ const { activeTasks, completedTasks } = loadTasks(projectDir);
288
+ const allTasks = [...activeTasks, ...completedTasks];
289
+
290
+ const taskId = getNextId(allTasks);
291
+ const today = new Date().toISOString().split('T')[0];
292
+
293
+ const newTask = {
294
+ id: taskId,
295
+ title: task.subject || 'Untitled',
296
+ status: task.status || 'pending',
297
+ owner: task.owner || null,
298
+ created: task.created || today,
299
+ completed: null,
300
+ story: task.story || null,
301
+ blockedBy: task.blockedBy || null,
302
+ description: task.description || null,
303
+ };
304
+
305
+ // Validate status
306
+ if (!VALID_STATUSES.includes(newTask.status)) {
307
+ return { ok: false, error: `Invalid status: ${newTask.status}` };
308
+ }
309
+
310
+ // Add to appropriate section
311
+ const newActiveTasks = newTask.status === 'completed' ? activeTasks : [...activeTasks, newTask];
312
+ const newCompletedTasks =
313
+ newTask.status === 'completed' ? [...completedTasks, newTask] : completedTasks;
314
+
315
+ if (
316
+ !saveTasks(projectDir, { activeTasks: newActiveTasks, completedTasks: newCompletedTasks })
317
+ ) {
318
+ return { ok: false, error: 'Failed to save tasks file' };
319
+ }
320
+
321
+ return { ok: true, taskId };
322
+ } catch (e) {
323
+ return { ok: false, error: e.message };
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Update a task
329
+ * @param {string} projectDir - Project directory
330
+ * @param {string} taskId - Task ID like T-001
331
+ * @param {Object} updates - Fields to update { status, owner, description, etc. }
332
+ * @returns {Object} { ok: boolean, error?: string }
333
+ */
334
+ function updateTask(projectDir, taskId, updates) {
335
+ try {
336
+ let { activeTasks, completedTasks } = loadTasks(projectDir);
337
+
338
+ // Find task in either list
339
+ let task = activeTasks.find(t => t.id === taskId);
340
+ let isInActive = !!task;
341
+
342
+ if (!task) {
343
+ task = completedTasks.find(t => t.id === taskId);
344
+ if (!task) {
345
+ return { ok: false, error: `Task ${taskId} not found` };
346
+ }
347
+ }
348
+
349
+ // Update fields
350
+ if (updates.status) {
351
+ if (!VALID_STATUSES.includes(updates.status)) {
352
+ return { ok: false, error: `Invalid status: ${updates.status}` };
353
+ }
354
+ task.status = updates.status;
355
+ }
356
+ if (updates.owner !== undefined) task.owner = updates.owner;
357
+ if (updates.description !== undefined) task.description = updates.description;
358
+ if (updates.title !== undefined) task.title = updates.title;
359
+ if (updates.story !== undefined) task.story = updates.story;
360
+ if (updates.blockedBy !== undefined) task.blockedBy = updates.blockedBy;
361
+ if (updates.completed !== undefined) task.completed = updates.completed;
362
+
363
+ // Move between sections if status changed
364
+ if (updates.status) {
365
+ const wasCompleted = !isInActive;
366
+ const isNowCompleted = updates.status === 'completed';
367
+
368
+ if (wasCompleted && !isNowCompleted) {
369
+ // Move from completed to active
370
+ completedTasks = completedTasks.filter(t => t.id !== taskId);
371
+ activeTasks.push(task);
372
+ } else if (!wasCompleted && isNowCompleted) {
373
+ // Move from active to completed
374
+ activeTasks = activeTasks.filter(t => t.id !== taskId);
375
+ // Set completion date if not already set
376
+ if (!task.completed) {
377
+ task.completed = new Date().toISOString().split('T')[0];
378
+ }
379
+ completedTasks.push(task);
380
+ }
381
+ }
382
+
383
+ if (!saveTasks(projectDir, { activeTasks, completedTasks })) {
384
+ return { ok: false, error: 'Failed to save tasks file' };
385
+ }
386
+
387
+ return { ok: true };
388
+ } catch (e) {
389
+ return { ok: false, error: e.message };
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Delete a task
395
+ * @param {string} projectDir - Project directory
396
+ * @param {string} taskId - Task ID like T-001
397
+ * @returns {Object} { ok: boolean, error?: string }
398
+ */
399
+ function deleteTask(projectDir, taskId) {
400
+ try {
401
+ let { activeTasks, completedTasks } = loadTasks(projectDir);
402
+
403
+ const foundInActive = activeTasks.some(t => t.id === taskId);
404
+ const foundInCompleted = completedTasks.some(t => t.id === taskId);
405
+
406
+ if (!foundInActive && !foundInCompleted) {
407
+ return { ok: false, error: `Task ${taskId} not found` };
408
+ }
409
+
410
+ activeTasks = activeTasks.filter(t => t.id !== taskId);
411
+ completedTasks = completedTasks.filter(t => t.id !== taskId);
412
+
413
+ if (!saveTasks(projectDir, { activeTasks, completedTasks })) {
414
+ return { ok: false, error: 'Failed to save tasks file' };
415
+ }
416
+
417
+ return { ok: true };
418
+ } catch (e) {
419
+ return { ok: false, error: e.message };
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Get a single task by ID
425
+ * @param {string} projectDir - Project directory
426
+ * @param {string} taskId - Task ID like T-001
427
+ * @returns {Object|null} Task object or null if not found
428
+ */
429
+ function getTask(projectDir, taskId) {
430
+ const { activeTasks, completedTasks } = loadTasks(projectDir);
431
+
432
+ const task = activeTasks.find(t => t.id === taskId) || completedTasks.find(t => t.id === taskId);
433
+ return task || null;
434
+ }
435
+
436
+ /**
437
+ * List tasks with optional filtering
438
+ * @param {string} projectDir - Project directory
439
+ * @param {Object} filters - { status, owner, includeCompleted } - defaults to active only
440
+ * @returns {Array} Array of task objects
441
+ */
442
+ function listTasks(projectDir, filters = {}) {
443
+ const { activeTasks, completedTasks } = loadTasks(projectDir);
444
+
445
+ let tasks = [...activeTasks];
446
+ if (filters.includeCompleted === true) {
447
+ tasks = [...activeTasks, ...completedTasks];
448
+ }
449
+
450
+ // Filter by status
451
+ if (filters.status) {
452
+ const statuses = Array.isArray(filters.status) ? filters.status : [filters.status];
453
+ tasks = tasks.filter(t => statuses.includes(t.status));
454
+ } else if (filters.includeCompleted !== true) {
455
+ // Default: show only active statuses
456
+ tasks = tasks.filter(t => STATUS_ACTIVE.includes(t.status));
457
+ }
458
+
459
+ // Filter by owner
460
+ if (filters.owner) {
461
+ tasks = tasks.filter(t => t.owner === filters.owner);
462
+ }
463
+
464
+ return tasks;
465
+ }
466
+
467
+ module.exports = {
468
+ loadTasks,
469
+ saveTasks,
470
+ addTask,
471
+ updateTask,
472
+ deleteTask,
473
+ getTask,
474
+ listTasks,
475
+ getNextId,
476
+ parseTasksFile,
477
+ formatTasksFile,
478
+ sanitizeField,
479
+ };
@@ -546,7 +546,7 @@ const FEATURE_DETECTORS = {
546
546
  priority: 'medium',
547
547
  trigger: `${coreFiles} source files modified - logic audit available`,
548
548
  action: 'offer',
549
- command: '/agileflow:logic:audit',
549
+ command: '/agileflow:audit:logic',
550
550
  phase: 'post-impl',
551
551
  });
552
552
  },
@@ -80,8 +80,62 @@ const EVENT_TYPES = [
80
80
  'gate_passed',
81
81
  'gate_failed',
82
82
  'model_usage',
83
+ 'cost_warning',
83
84
  ];
84
85
 
86
+ /**
87
+ * Model pricing per million tokens (USD).
88
+ * Includes both shorthand aliases and full model IDs.
89
+ */
90
+ const MODEL_PRICING = {
91
+ haiku: { input: 0.8, output: 4.0 },
92
+ sonnet: { input: 3.0, output: 15.0 },
93
+ opus: { input: 15.0, output: 75.0 },
94
+ 'claude-haiku-4-5-20251001': { input: 0.8, output: 4.0 },
95
+ 'claude-sonnet-4-6': { input: 3.0, output: 15.0 },
96
+ 'claude-opus-4-6': { input: 15.0, output: 75.0 },
97
+ };
98
+
99
+ const DEFAULT_COST_THRESHOLD_USD = 5.0;
100
+
101
+ /**
102
+ * Compute estimated cost for an agent's token usage.
103
+ *
104
+ * @param {number} inputTokens - Number of input tokens
105
+ * @param {number} outputTokens - Number of output tokens
106
+ * @param {string} [model='haiku'] - Model name or alias
107
+ * @returns {number} Estimated cost in USD (6 decimal places)
108
+ */
109
+ function computeAgentCost(inputTokens, outputTokens, model) {
110
+ const pricing = MODEL_PRICING[model] || MODEL_PRICING['haiku'];
111
+ const inputCost = (inputTokens / 1_000_000) * pricing.input;
112
+ const outputCost = (outputTokens / 1_000_000) * pricing.output;
113
+ return Math.round((inputCost + outputCost) * 1_000_000) / 1_000_000;
114
+ }
115
+
116
+ /**
117
+ * Check if team cost exceeds threshold and emit warning event.
118
+ *
119
+ * @param {string} rootDir - Project root directory
120
+ * @param {string} traceId - Trace ID for the team run
121
+ * @param {number} totalCostUsd - Total cost in USD
122
+ * @param {number} [threshold] - Cost threshold in USD (default: DEFAULT_COST_THRESHOLD_USD)
123
+ * @returns {boolean} True if threshold was exceeded
124
+ */
125
+ function checkCostThreshold(rootDir, traceId, totalCostUsd, threshold) {
126
+ const limit = threshold || DEFAULT_COST_THRESHOLD_USD;
127
+ if (totalCostUsd > limit) {
128
+ trackEvent(rootDir, 'cost_warning', {
129
+ trace_id: traceId,
130
+ total_cost_usd: totalCostUsd,
131
+ threshold_usd: limit,
132
+ message: `Team cost $${totalCostUsd.toFixed(4)} exceeds threshold $${limit.toFixed(2)}`,
133
+ });
134
+ return true;
135
+ }
136
+ return false;
137
+ }
138
+
85
139
  /**
86
140
  * Track an agent teams event.
87
141
  *
@@ -232,15 +286,29 @@ function aggregateTeamMetrics(rootDir, traceId) {
232
286
  const perAgent = {};
233
287
  const ensureAgent = agent => {
234
288
  if (!perAgent[agent]) {
235
- perAgent[agent] = { total_duration_ms: 0, tasks_completed: 0, errors: 0, timeouts: 0 };
289
+ perAgent[agent] = {
290
+ total_duration_ms: 0,
291
+ tasks_completed: 0,
292
+ errors: 0,
293
+ timeouts: 0,
294
+ input_tokens: 0,
295
+ output_tokens: 0,
296
+ cost_usd: 0,
297
+ };
236
298
  }
237
299
  };
238
300
 
301
+ // Track model per agent for cost computation
302
+ const agentModels = {};
303
+
239
304
  for (const e of events) {
240
305
  if (e.type === 'task_completed' && e.agent) {
241
306
  ensureAgent(e.agent);
242
307
  perAgent[e.agent].total_duration_ms += e.duration_ms || 0;
243
308
  perAgent[e.agent].tasks_completed++;
309
+ perAgent[e.agent].input_tokens += e.input_tokens || 0;
310
+ perAgent[e.agent].output_tokens += e.output_tokens || 0;
311
+ if (e.model) agentModels[e.agent] = e.model;
244
312
  }
245
313
  if (e.type === 'agent_error' && e.agent) {
246
314
  ensureAgent(e.agent);
@@ -252,6 +320,16 @@ function aggregateTeamMetrics(rootDir, traceId) {
252
320
  }
253
321
  }
254
322
 
323
+ // Compute per-agent costs
324
+ for (const [agent, metrics] of Object.entries(perAgent)) {
325
+ metrics.cost_usd = computeAgentCost(
326
+ metrics.input_tokens,
327
+ metrics.output_tokens,
328
+ agentModels[agent] || 'haiku'
329
+ );
330
+ }
331
+ const totalCostUsd = Object.values(perAgent).reduce((sum, a) => sum + a.cost_usd, 0);
332
+
255
333
  // Per-gate metrics from gate_passed, gate_failed
256
334
  const perGate = {};
257
335
  for (const e of events) {
@@ -281,6 +359,7 @@ function aggregateTeamMetrics(rootDir, traceId) {
281
359
  per_agent: perAgent,
282
360
  per_gate: perGate,
283
361
  team_completion_ms: teamCompletionMs,
362
+ total_cost_usd: Math.round(totalCostUsd * 1_000_000) / 1_000_000,
284
363
  computed_at: new Date().toISOString(),
285
364
  };
286
365
  }
@@ -313,6 +392,7 @@ function saveAggregatedMetrics(rootDir, metrics) {
313
392
  per_agent: metrics.per_agent,
314
393
  per_gate: metrics.per_gate,
315
394
  team_completion_ms: metrics.team_completion_ms,
395
+ total_cost_usd: metrics.total_cost_usd,
316
396
  computed_at: metrics.computed_at,
317
397
  };
318
398
  return state;
@@ -329,6 +409,7 @@ function saveAggregatedMetrics(rootDir, metrics) {
329
409
  per_agent: metrics.per_agent,
330
410
  per_gate: metrics.per_gate,
331
411
  team_completion_ms: metrics.team_completion_ms,
412
+ total_cost_usd: metrics.total_cost_usd,
332
413
  computed_at: metrics.computed_at,
333
414
  };
334
415
  fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
@@ -349,9 +430,13 @@ function saveAggregatedMetrics(rootDir, metrics) {
349
430
 
350
431
  module.exports = {
351
432
  EVENT_TYPES,
433
+ MODEL_PRICING,
434
+ DEFAULT_COST_THRESHOLD_USD,
352
435
  trackEvent,
353
436
  getTeamEvents,
354
437
  aggregateTeamMetrics,
355
438
  saveAggregatedMetrics,
439
+ computeAgentCost,
440
+ checkCostThreshold,
356
441
  teamMetricsEmitter,
357
442
  };
@@ -184,20 +184,25 @@ async function main() {
184
184
  const metadata = safeReadJSON('docs/00-meta/agileflow-metadata.json');
185
185
  const lazyConfig = metadata?.features?.lazyContext;
186
186
 
187
+ // Read context verbosity mode
188
+ const verbosityMode = metadata?.features?.contextVerbosity?.enabled
189
+ ? metadata.features.contextVerbosity.mode || 'full'
190
+ : 'full';
191
+
187
192
  // Determine which sections need full content (US-0093)
188
193
  const sectionsToLoad = determineSectionsToLoad(commandName, lazyConfig, isMultiSession);
189
194
 
190
195
  // Pre-fetch all data in parallel
191
196
  const prefetchStart = Date.now();
192
- const prefetched = await prefetchAllData({ sectionsToLoad });
197
+ const prefetched = await prefetchAllData({ sectionsToLoad, verbosityMode });
193
198
  const prefetchElapsed = Date.now() - prefetchStart;
194
199
  if (prefetchElapsed > 400) {
195
200
  process.stderr.write(`Context loaded in ${(prefetchElapsed / 1000).toFixed(1)}s\n`);
196
201
  }
197
202
 
198
- // Run smart detection (contextual feature routing)
203
+ // Run smart detection (contextual feature routing) - skip in minimal mode
199
204
  let smartDetectResults = null;
200
- if (smartDetect) {
205
+ if (smartDetect && verbosityMode !== 'minimal') {
201
206
  try {
202
207
  smartDetectResults = smartDetect.analyze(prefetched);
203
208
  smartDetect.writeRecommendations(smartDetectResults);
@@ -206,8 +211,27 @@ async function main() {
206
211
  }
207
212
  }
208
213
 
214
+ // Check command prerequisites
215
+ let prereqResult = null;
216
+ if (commandName && smartDetect) {
217
+ try {
218
+ const { checkCommandPrereqs, loadPrereqConfig } = require('./lib/command-prereqs');
219
+ const config = loadPrereqConfig();
220
+ const signals = smartDetect.extractSignals(prefetched);
221
+ prereqResult = checkCommandPrereqs(commandName, signals, config);
222
+ } catch {
223
+ // Fail open - prereq checking is optional
224
+ }
225
+ }
226
+
209
227
  // Generate formatted output
210
- const formatOptions = { commandName, activeSections, smartDetectResults };
228
+ const formatOptions = {
229
+ commandName,
230
+ activeSections,
231
+ smartDetectResults,
232
+ prereqResult,
233
+ verbosityMode,
234
+ };
211
235
  const summary = generateSummary(prefetched, formatOptions);
212
236
  const fullContent = generateFullContent(prefetched, formatOptions);
213
237