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.
- package/CHANGELOG.md +5 -0
- package/README.md +57 -85
- package/lib/dashboard-automations.js +130 -0
- package/lib/dashboard-git.js +254 -0
- package/lib/dashboard-inbox.js +64 -0
- package/lib/dashboard-protocol.js +1 -0
- package/lib/dashboard-server.js +114 -924
- package/lib/dashboard-session.js +136 -0
- package/lib/dashboard-status.js +72 -0
- package/lib/dashboard-terminal.js +354 -0
- package/lib/dashboard-websocket.js +88 -0
- package/lib/drivers/codex-driver.ts +4 -4
- package/lib/logger.js +106 -0
- package/package.json +4 -2
- package/scripts/agileflow-configure.js +2 -2
- package/scripts/agileflow-welcome.js +409 -434
- package/scripts/claude-tmux.sh +80 -2
- package/scripts/context-loader.js +4 -9
- package/scripts/lib/command-prereqs.js +280 -0
- package/scripts/lib/configure-detect.js +92 -2
- package/scripts/lib/configure-features.js +295 -1
- package/scripts/lib/context-formatter.js +468 -233
- package/scripts/lib/context-loader.js +27 -15
- package/scripts/lib/damage-control-utils.js +8 -1
- package/scripts/lib/feature-catalog.js +321 -0
- package/scripts/lib/portable-tasks-cli.js +274 -0
- package/scripts/lib/portable-tasks.js +479 -0
- package/scripts/lib/signal-detectors.js +1 -1
- package/scripts/lib/team-events.js +86 -1
- package/scripts/obtain-context.js +28 -4
- package/scripts/smart-detect.js +17 -0
- package/scripts/strip-ai-attribution.js +63 -0
- package/scripts/team-manager.js +7 -2
- package/scripts/welcome-deferred.js +437 -0
- package/src/core/agents/perf-analyzer-assets.md +174 -0
- package/src/core/agents/perf-analyzer-bundle.md +165 -0
- package/src/core/agents/perf-analyzer-caching.md +160 -0
- package/src/core/agents/perf-analyzer-compute.md +165 -0
- package/src/core/agents/perf-analyzer-memory.md +182 -0
- package/src/core/agents/perf-analyzer-network.md +157 -0
- package/src/core/agents/perf-analyzer-queries.md +155 -0
- package/src/core/agents/perf-analyzer-rendering.md +156 -0
- package/src/core/agents/perf-consensus.md +280 -0
- package/src/core/agents/security-analyzer-api.md +199 -0
- package/src/core/agents/security-analyzer-auth.md +160 -0
- package/src/core/agents/security-analyzer-authz.md +168 -0
- package/src/core/agents/security-analyzer-deps.md +147 -0
- package/src/core/agents/security-analyzer-infra.md +176 -0
- package/src/core/agents/security-analyzer-injection.md +148 -0
- package/src/core/agents/security-analyzer-input.md +191 -0
- package/src/core/agents/security-analyzer-secrets.md +175 -0
- package/src/core/agents/security-consensus.md +276 -0
- package/src/core/agents/test-analyzer-assertions.md +181 -0
- package/src/core/agents/test-analyzer-coverage.md +183 -0
- package/src/core/agents/test-analyzer-fragility.md +185 -0
- package/src/core/agents/test-analyzer-integration.md +155 -0
- package/src/core/agents/test-analyzer-maintenance.md +173 -0
- package/src/core/agents/test-analyzer-mocking.md +178 -0
- package/src/core/agents/test-analyzer-patterns.md +189 -0
- package/src/core/agents/test-analyzer-structure.md +177 -0
- package/src/core/agents/test-consensus.md +294 -0
- package/src/core/commands/{legal/audit.md → audit/legal.md} +13 -13
- package/src/core/commands/{logic/audit.md → audit/logic.md} +12 -12
- package/src/core/commands/audit/performance.md +443 -0
- package/src/core/commands/audit/security.md +443 -0
- package/src/core/commands/audit/test.md +442 -0
- package/src/core/commands/babysit.md +505 -463
- package/src/core/commands/configure.md +8 -8
- package/src/core/commands/research/ask.md +42 -9
- package/src/core/commands/research/import.md +14 -8
- package/src/core/commands/research/list.md +17 -16
- package/src/core/commands/research/synthesize.md +8 -8
- package/src/core/commands/research/view.md +28 -4
- package/src/core/commands/whats-new.md +2 -2
- package/src/core/experts/devops/expertise.yaml +13 -2
- package/src/core/experts/documentation/expertise.yaml +26 -4
- package/src/core/profiles/COMPARISON.md +170 -0
- package/src/core/profiles/README.md +178 -0
- package/src/core/profiles/claude-code.yaml +111 -0
- package/src/core/profiles/codex.yaml +103 -0
- package/src/core/profiles/cursor.yaml +134 -0
- package/src/core/profiles/examples.js +250 -0
- package/src/core/profiles/loader.js +235 -0
- package/src/core/profiles/windsurf.yaml +159 -0
- package/src/core/teams/logic-audit.json +6 -0
- package/src/core/teams/perf-audit.json +71 -0
- package/src/core/teams/security-audit.json +71 -0
- package/src/core/teams/test-audit.json +71 -0
- package/src/core/templates/command-prerequisites.yaml +169 -0
- package/src/core/templates/damage-control-patterns.yaml +9 -0
- package/tools/cli/installers/ide/_base-ide.js +33 -3
- package/tools/cli/installers/ide/claude-code.js +2 -69
- package/tools/cli/installers/ide/codex.js +9 -9
- package/tools/cli/installers/ide/cursor.js +165 -4
- package/tools/cli/installers/ide/windsurf.js +237 -6
- package/tools/cli/lib/content-transformer.js +234 -9
- package/tools/cli/lib/docs-setup.js +1 -1
- package/tools/cli/lib/ide-generator.js +357 -0
- package/tools/cli/lib/ide-registry.js +2 -2
- package/scripts/tmux-task-name.sh +0 -105
- 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
|
|
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] = {
|
|
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 = {
|
|
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
|
|