agileflow 2.99.8 → 3.0.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/lib/cache-provider.js +155 -0
- package/lib/codebase-indexer.js +1 -1
- package/lib/content-sanitizer.js +1 -0
- package/lib/dashboard-protocol.js +25 -0
- package/lib/dashboard-server.js +184 -133
- package/lib/errors.js +18 -0
- package/lib/file-cache.js +1 -1
- package/lib/flag-detection.js +11 -20
- package/lib/git-operations.js +15 -33
- package/lib/merge-operations.js +40 -34
- package/lib/process-executor.js +199 -0
- package/lib/registry-cache.js +13 -47
- package/lib/skill-loader.js +206 -0
- package/lib/smart-json-file.js +2 -4
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +13 -12
- package/scripts/agileflow-statusline.sh +30 -0
- package/scripts/agileflow-welcome.js +181 -212
- package/scripts/auto-self-improve.js +3 -3
- package/scripts/claude-smart.sh +67 -0
- package/scripts/claude-tmux.sh +248 -161
- package/scripts/damage-control-multi-agent.js +227 -0
- package/scripts/lib/bus-utils.js +471 -0
- package/scripts/lib/configure-detect.js +5 -6
- package/scripts/lib/configure-features.js +44 -0
- package/scripts/lib/configure-repair.js +5 -6
- package/scripts/lib/configure-utils.js +2 -3
- package/scripts/lib/context-formatter.js +87 -8
- package/scripts/lib/damage-control-utils.js +37 -3
- package/scripts/lib/file-lock.js +392 -0
- package/scripts/lib/ideation-index.js +2 -5
- package/scripts/lib/lifecycle-detector.js +123 -0
- package/scripts/lib/process-cleanup.js +55 -81
- package/scripts/lib/scale-detector.js +357 -0
- package/scripts/lib/signal-detectors.js +779 -0
- package/scripts/lib/story-state-machine.js +1 -1
- package/scripts/lib/sync-ideation-status.js +2 -3
- package/scripts/lib/task-registry.js +7 -1
- package/scripts/lib/team-events.js +357 -0
- package/scripts/messaging-bridge.js +79 -36
- package/scripts/migrate-ideation-index.js +37 -14
- package/scripts/obtain-context.js +37 -19
- package/scripts/ralph-loop.js +3 -4
- package/scripts/smart-detect.js +390 -0
- package/scripts/team-manager.js +174 -30
- package/src/core/commands/audit.md +13 -11
- package/src/core/commands/babysit.md +162 -115
- package/src/core/commands/changelog.md +21 -4
- package/src/core/commands/configure.md +105 -2
- package/src/core/commands/debt.md +12 -2
- package/src/core/commands/feedback.md +7 -6
- package/src/core/commands/ideate/history.md +1 -1
- package/src/core/commands/ideate/new.md +5 -5
- package/src/core/commands/logic/audit.md +2 -2
- package/src/core/commands/pr.md +7 -6
- package/src/core/commands/research/analyze.md +28 -20
- package/src/core/commands/research/ask.md +43 -0
- package/src/core/commands/research/import.md +29 -21
- package/src/core/commands/research/list.md +8 -7
- package/src/core/commands/research/synthesize.md +356 -20
- package/src/core/commands/research/view.md +8 -5
- package/src/core/commands/review.md +24 -6
- package/src/core/commands/skill/create.md +34 -0
- package/tools/cli/lib/docs-setup.js +4 -0
|
@@ -325,7 +325,7 @@ function checkEpicCompletion(statusData, epicId) {
|
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
const storyIds = epic.stories || [];
|
|
328
|
-
const completedStatuses =
|
|
328
|
+
const completedStatuses = COMPLETED_STATUSES;
|
|
329
329
|
const completed = [];
|
|
330
330
|
const remaining = [];
|
|
331
331
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
|
+
const { tryOptional } = require('../../lib/errors');
|
|
15
16
|
|
|
16
17
|
// Paths relative to project root
|
|
17
18
|
const STATUS_PATH = 'docs/09-agents/status.json';
|
|
@@ -49,9 +50,7 @@ function saveJSON(filePath, data) {
|
|
|
49
50
|
fs.renameSync(tempPath, filePath);
|
|
50
51
|
return { ok: true };
|
|
51
52
|
} catch (err) {
|
|
52
|
-
|
|
53
|
-
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
54
|
-
} catch {}
|
|
53
|
+
tryOptional(() => fs.unlinkSync(tempPath), 'cleanup temp');
|
|
55
54
|
return { ok: false, error: `Failed to save ${filePath}: ${err.message}` };
|
|
56
55
|
}
|
|
57
56
|
}
|
|
@@ -332,6 +332,7 @@ class FileLock {
|
|
|
332
332
|
}
|
|
333
333
|
|
|
334
334
|
// Try to acquire
|
|
335
|
+
let fd = null;
|
|
335
336
|
try {
|
|
336
337
|
const dir = path.dirname(this.lockPath);
|
|
337
338
|
if (!fs.existsSync(dir)) {
|
|
@@ -339,7 +340,7 @@ class FileLock {
|
|
|
339
340
|
}
|
|
340
341
|
|
|
341
342
|
// Use exclusive flag to prevent race
|
|
342
|
-
|
|
343
|
+
fd = fs.openSync(this.lockPath, 'wx');
|
|
343
344
|
const lockInfo = {
|
|
344
345
|
pid: process.pid,
|
|
345
346
|
hostname: os.hostname(),
|
|
@@ -347,9 +348,14 @@ class FileLock {
|
|
|
347
348
|
};
|
|
348
349
|
fs.writeSync(fd, JSON.stringify(lockInfo));
|
|
349
350
|
fs.closeSync(fd);
|
|
351
|
+
fd = null;
|
|
350
352
|
this.held = true;
|
|
351
353
|
return true;
|
|
352
354
|
} catch (e) {
|
|
355
|
+
// Clean up fd if still open
|
|
356
|
+
if (fd !== null) {
|
|
357
|
+
try { fs.closeSync(fd); } catch (_) { /* ignore */ }
|
|
358
|
+
}
|
|
353
359
|
if (e.code === 'EEXIST') {
|
|
354
360
|
// Another process got the lock
|
|
355
361
|
this._sleep(50);
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* team-events.js - Observability event tracking for Agent Teams
|
|
3
|
+
*
|
|
4
|
+
* Captures team lifecycle events and stores them in session-state.json
|
|
5
|
+
* and the JSONL message bus. Events include:
|
|
6
|
+
* - team_created: Team started from template
|
|
7
|
+
* - team_stopped: Team shut down
|
|
8
|
+
* - task_assigned: Task given to a teammate
|
|
9
|
+
* - task_completed: Task finished by a teammate
|
|
10
|
+
* - agent_error: Teammate encountered an error
|
|
11
|
+
* - agent_timeout: Teammate exceeded time limit
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const { trackEvent, getTeamEvents } = require('./lib/team-events');
|
|
15
|
+
*
|
|
16
|
+
* trackEvent(rootDir, 'task_completed', {
|
|
17
|
+
* agent: 'api-builder',
|
|
18
|
+
* task_id: 'task_1',
|
|
19
|
+
* duration_ms: 5200,
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const { EventEmitter } = require('events');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Module-level event emitter for cross-module notifications.
|
|
29
|
+
* Emits 'metrics_saved' after saveAggregatedMetrics() succeeds.
|
|
30
|
+
*/
|
|
31
|
+
const teamMetricsEmitter = new EventEmitter();
|
|
32
|
+
|
|
33
|
+
// Lazy-load dependencies
|
|
34
|
+
let _fileLock;
|
|
35
|
+
function getFileLock() {
|
|
36
|
+
if (!_fileLock) {
|
|
37
|
+
try {
|
|
38
|
+
_fileLock = require('./file-lock');
|
|
39
|
+
} catch (e) {
|
|
40
|
+
_fileLock = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return _fileLock;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let _messagingBridge;
|
|
47
|
+
function getMessagingBridge() {
|
|
48
|
+
if (!_messagingBridge) {
|
|
49
|
+
try {
|
|
50
|
+
_messagingBridge = require('../messaging-bridge');
|
|
51
|
+
} catch (e) {
|
|
52
|
+
_messagingBridge = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return _messagingBridge;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let _paths;
|
|
59
|
+
function getPaths() {
|
|
60
|
+
if (!_paths) {
|
|
61
|
+
try {
|
|
62
|
+
_paths = require('../../lib/paths');
|
|
63
|
+
} catch (e) {
|
|
64
|
+
_paths = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return _paths;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Valid event types for agent teams
|
|
72
|
+
*/
|
|
73
|
+
const EVENT_TYPES = [
|
|
74
|
+
'team_created',
|
|
75
|
+
'team_stopped',
|
|
76
|
+
'task_assigned',
|
|
77
|
+
'task_completed',
|
|
78
|
+
'agent_error',
|
|
79
|
+
'agent_timeout',
|
|
80
|
+
'gate_passed',
|
|
81
|
+
'gate_failed',
|
|
82
|
+
'model_usage',
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Track an agent teams event.
|
|
87
|
+
*
|
|
88
|
+
* Writes to both:
|
|
89
|
+
* 1. session-state.json (hook_metrics.teams section) for real-time status
|
|
90
|
+
* 2. JSONL bus (via messaging-bridge) for historical audit trail
|
|
91
|
+
*
|
|
92
|
+
* @param {string} rootDir - Project root directory
|
|
93
|
+
* @param {string} eventType - Event type (see EVENT_TYPES)
|
|
94
|
+
* @param {object} data - Event data (agent, task_id, duration_ms, etc.)
|
|
95
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
96
|
+
*/
|
|
97
|
+
function trackEvent(rootDir, eventType, data = {}) {
|
|
98
|
+
const event = {
|
|
99
|
+
type: eventType,
|
|
100
|
+
at: new Date().toISOString(),
|
|
101
|
+
...data,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// 1. Update session-state.json
|
|
105
|
+
try {
|
|
106
|
+
const paths = getPaths();
|
|
107
|
+
const sessionStatePath = paths
|
|
108
|
+
? paths.getSessionStatePath(rootDir)
|
|
109
|
+
: path.join(rootDir, 'docs', '09-agents', 'session-state.json');
|
|
110
|
+
|
|
111
|
+
const fileLock = getFileLock();
|
|
112
|
+
|
|
113
|
+
if (fileLock && fs.existsSync(sessionStatePath)) {
|
|
114
|
+
fileLock.atomicReadModifyWrite(sessionStatePath, (state) => {
|
|
115
|
+
if (!state.hook_metrics) state.hook_metrics = {};
|
|
116
|
+
if (!state.hook_metrics.teams) state.hook_metrics.teams = { events: [], summary: {} };
|
|
117
|
+
|
|
118
|
+
const teams = state.hook_metrics.teams;
|
|
119
|
+
|
|
120
|
+
// Append to events (keep last 50)
|
|
121
|
+
teams.events.push(event);
|
|
122
|
+
if (teams.events.length > 50) {
|
|
123
|
+
teams.events = teams.events.slice(-50);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Update summary counters
|
|
127
|
+
if (!teams.summary[eventType]) teams.summary[eventType] = 0;
|
|
128
|
+
teams.summary[eventType]++;
|
|
129
|
+
|
|
130
|
+
teams.last_updated = event.at;
|
|
131
|
+
|
|
132
|
+
return state;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
} catch (e) {
|
|
136
|
+
// Non-critical - continue to bus logging
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2. Log to JSONL bus
|
|
140
|
+
try {
|
|
141
|
+
const bridge = getMessagingBridge();
|
|
142
|
+
if (bridge) {
|
|
143
|
+
bridge.sendMessage(rootDir, {
|
|
144
|
+
from: data.agent || 'team-events',
|
|
145
|
+
to: 'observability',
|
|
146
|
+
type: eventType,
|
|
147
|
+
...data,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
} catch (e) {
|
|
151
|
+
// Non-critical
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { ok: true };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get team events from session-state.json.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} rootDir - Project root directory
|
|
161
|
+
* @param {object} [filters] - Optional filters { type, agent, trace_id, since, limit }
|
|
162
|
+
* @returns {{ ok: boolean, events?: Array, summary?: object }}
|
|
163
|
+
*/
|
|
164
|
+
function getTeamEvents(rootDir, filters = {}) {
|
|
165
|
+
try {
|
|
166
|
+
const paths = getPaths();
|
|
167
|
+
const sessionStatePath = paths
|
|
168
|
+
? paths.getSessionStatePath(rootDir)
|
|
169
|
+
: path.join(rootDir, 'docs', '09-agents', 'session-state.json');
|
|
170
|
+
|
|
171
|
+
if (!fs.existsSync(sessionStatePath)) {
|
|
172
|
+
return { ok: true, events: [], summary: {} };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
176
|
+
const teams = state.hook_metrics && state.hook_metrics.teams;
|
|
177
|
+
if (!teams) {
|
|
178
|
+
return { ok: true, events: [], summary: {} };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let events = teams.events || [];
|
|
182
|
+
|
|
183
|
+
// Apply filters
|
|
184
|
+
if (filters.type) {
|
|
185
|
+
events = events.filter(e => e.type === filters.type);
|
|
186
|
+
}
|
|
187
|
+
if (filters.agent) {
|
|
188
|
+
events = events.filter(e => e.agent === filters.agent);
|
|
189
|
+
}
|
|
190
|
+
if (filters.trace_id) {
|
|
191
|
+
events = events.filter(e => e.trace_id === filters.trace_id);
|
|
192
|
+
}
|
|
193
|
+
if (filters.since) {
|
|
194
|
+
const sinceTime = new Date(filters.since).getTime();
|
|
195
|
+
events = events.filter(e => new Date(e.at).getTime() >= sinceTime);
|
|
196
|
+
}
|
|
197
|
+
if (filters.limit && filters.limit > 0) {
|
|
198
|
+
events = events.slice(-filters.limit);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
events,
|
|
204
|
+
summary: teams.summary || {},
|
|
205
|
+
};
|
|
206
|
+
} catch (e) {
|
|
207
|
+
return { ok: false, error: e.message, events: [] };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Aggregate team metrics from events filtered by trace_id.
|
|
213
|
+
*
|
|
214
|
+
* Computes:
|
|
215
|
+
* - Per-agent: total duration, tasks completed, errors, timeouts
|
|
216
|
+
* - Per-gate: passed/failed counts and pass rate
|
|
217
|
+
* - Team completion time (team_created → team_stopped)
|
|
218
|
+
*
|
|
219
|
+
* @param {string} rootDir - Project root directory
|
|
220
|
+
* @param {string} traceId - Trace ID to aggregate metrics for
|
|
221
|
+
* @returns {{ ok: boolean, trace_id: string, per_agent: object, per_gate: object, team_completion_ms: number|null, computed_at: string }}
|
|
222
|
+
*/
|
|
223
|
+
function aggregateTeamMetrics(rootDir, traceId) {
|
|
224
|
+
if (!traceId) {
|
|
225
|
+
return { ok: false, error: 'trace_id is required' };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const result = getTeamEvents(rootDir, { trace_id: traceId });
|
|
229
|
+
const events = result.events || [];
|
|
230
|
+
|
|
231
|
+
// Per-agent metrics from task_completed, agent_error, agent_timeout
|
|
232
|
+
const perAgent = {};
|
|
233
|
+
const ensureAgent = (agent) => {
|
|
234
|
+
if (!perAgent[agent]) {
|
|
235
|
+
perAgent[agent] = { total_duration_ms: 0, tasks_completed: 0, errors: 0, timeouts: 0 };
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
for (const e of events) {
|
|
240
|
+
if (e.type === 'task_completed' && e.agent) {
|
|
241
|
+
ensureAgent(e.agent);
|
|
242
|
+
perAgent[e.agent].total_duration_ms += (e.duration_ms || 0);
|
|
243
|
+
perAgent[e.agent].tasks_completed++;
|
|
244
|
+
}
|
|
245
|
+
if (e.type === 'agent_error' && e.agent) {
|
|
246
|
+
ensureAgent(e.agent);
|
|
247
|
+
perAgent[e.agent].errors++;
|
|
248
|
+
}
|
|
249
|
+
if (e.type === 'agent_timeout' && e.agent) {
|
|
250
|
+
ensureAgent(e.agent);
|
|
251
|
+
perAgent[e.agent].timeouts++;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Per-gate metrics from gate_passed, gate_failed
|
|
256
|
+
const perGate = {};
|
|
257
|
+
for (const e of events) {
|
|
258
|
+
if (e.type === 'gate_passed' || e.type === 'gate_failed') {
|
|
259
|
+
const gateType = e.gate || 'unknown';
|
|
260
|
+
if (!perGate[gateType]) perGate[gateType] = { passed: 0, failed: 0, pass_rate: 0 };
|
|
261
|
+
if (e.type === 'gate_passed') perGate[gateType].passed++;
|
|
262
|
+
else perGate[gateType].failed++;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const gate of Object.keys(perGate)) {
|
|
266
|
+
const total = perGate[gate].passed + perGate[gate].failed;
|
|
267
|
+
perGate[gate].pass_rate = total > 0 ? perGate[gate].passed / total : 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Team completion time from team_created → team_stopped
|
|
271
|
+
let teamCompletionMs = null;
|
|
272
|
+
const created = events.find(e => e.type === 'team_created');
|
|
273
|
+
const stopped = events.find(e => e.type === 'team_stopped');
|
|
274
|
+
if (created && stopped) {
|
|
275
|
+
teamCompletionMs = new Date(stopped.at).getTime() - new Date(created.at).getTime();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
ok: true,
|
|
280
|
+
trace_id: traceId,
|
|
281
|
+
per_agent: perAgent,
|
|
282
|
+
per_gate: perGate,
|
|
283
|
+
team_completion_ms: teamCompletionMs,
|
|
284
|
+
computed_at: new Date().toISOString(),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Save aggregated metrics to session-state.json under team_metrics.traces[trace_id].
|
|
290
|
+
*
|
|
291
|
+
* @param {string} rootDir - Project root directory
|
|
292
|
+
* @param {object} metrics - Output from aggregateTeamMetrics()
|
|
293
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
294
|
+
*/
|
|
295
|
+
function saveAggregatedMetrics(rootDir, metrics) {
|
|
296
|
+
if (!metrics || !metrics.trace_id) {
|
|
297
|
+
return { ok: false, error: 'metrics with trace_id required' };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const paths = getPaths();
|
|
302
|
+
const sessionStatePath = paths
|
|
303
|
+
? paths.getSessionStatePath(rootDir)
|
|
304
|
+
: path.join(rootDir, 'docs', '09-agents', 'session-state.json');
|
|
305
|
+
|
|
306
|
+
const fileLock = getFileLock();
|
|
307
|
+
|
|
308
|
+
if (fileLock && fs.existsSync(sessionStatePath)) {
|
|
309
|
+
fileLock.atomicReadModifyWrite(sessionStatePath, (state) => {
|
|
310
|
+
if (!state.team_metrics) state.team_metrics = {};
|
|
311
|
+
if (!state.team_metrics.traces) state.team_metrics.traces = {};
|
|
312
|
+
state.team_metrics.traces[metrics.trace_id] = {
|
|
313
|
+
per_agent: metrics.per_agent,
|
|
314
|
+
per_gate: metrics.per_gate,
|
|
315
|
+
team_completion_ms: metrics.team_completion_ms,
|
|
316
|
+
computed_at: metrics.computed_at,
|
|
317
|
+
};
|
|
318
|
+
return state;
|
|
319
|
+
});
|
|
320
|
+
} else {
|
|
321
|
+
// Fallback to direct write
|
|
322
|
+
let state = {};
|
|
323
|
+
if (fs.existsSync(sessionStatePath)) {
|
|
324
|
+
state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
325
|
+
}
|
|
326
|
+
if (!state.team_metrics) state.team_metrics = {};
|
|
327
|
+
if (!state.team_metrics.traces) state.team_metrics.traces = {};
|
|
328
|
+
state.team_metrics.traces[metrics.trace_id] = {
|
|
329
|
+
per_agent: metrics.per_agent,
|
|
330
|
+
per_gate: metrics.per_gate,
|
|
331
|
+
team_completion_ms: metrics.team_completion_ms,
|
|
332
|
+
computed_at: metrics.computed_at,
|
|
333
|
+
};
|
|
334
|
+
fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Notify listeners (e.g., dashboard server) that metrics were saved
|
|
338
|
+
try {
|
|
339
|
+
teamMetricsEmitter.emit('metrics_saved', { trace_id: metrics.trace_id });
|
|
340
|
+
} catch (_) {
|
|
341
|
+
// Non-critical
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { ok: true };
|
|
345
|
+
} catch (e) {
|
|
346
|
+
return { ok: false, error: e.message };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
module.exports = {
|
|
351
|
+
EVENT_TYPES,
|
|
352
|
+
trackEvent,
|
|
353
|
+
getTeamEvents,
|
|
354
|
+
aggregateTeamMetrics,
|
|
355
|
+
saveAggregatedMetrics,
|
|
356
|
+
teamMetricsEmitter,
|
|
357
|
+
};
|
|
@@ -49,6 +49,18 @@ function getFeatureFlags() {
|
|
|
49
49
|
return _featureFlags;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
let _busUtils;
|
|
53
|
+
function getBusUtils() {
|
|
54
|
+
if (!_busUtils) {
|
|
55
|
+
try {
|
|
56
|
+
_busUtils = require('./lib/bus-utils');
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return _busUtils;
|
|
62
|
+
}
|
|
63
|
+
|
|
52
64
|
/**
|
|
53
65
|
* Get the bus log path.
|
|
54
66
|
*/
|
|
@@ -74,28 +86,75 @@ function ensureBusDir(rootDir) {
|
|
|
74
86
|
/**
|
|
75
87
|
* Send a message to the AgileFlow bus.
|
|
76
88
|
*
|
|
89
|
+
* When Agent Teams is enabled (native mode), the message is formatted for
|
|
90
|
+
* both the JSONL bus AND the native SendMessage channel. The JSONL bus
|
|
91
|
+
* remains the source of truth; native messaging is supplementary.
|
|
92
|
+
*
|
|
93
|
+
* Also triggers log rotation when the bus exceeds 1000 lines.
|
|
94
|
+
*
|
|
77
95
|
* @param {string} rootDir - Project root
|
|
78
96
|
* @param {object} message - Message object { from, to, type, ... }
|
|
79
|
-
* @returns {{ ok: boolean }}
|
|
97
|
+
* @returns {{ ok: boolean, native?: boolean }}
|
|
80
98
|
*/
|
|
81
99
|
function sendMessage(rootDir, message) {
|
|
82
100
|
try {
|
|
83
101
|
ensureBusDir(rootDir);
|
|
84
102
|
const logPath = getBusLogPath(rootDir);
|
|
103
|
+
const isNative = getFeatureFlags().isAgentTeamsEnabled({ rootDir });
|
|
85
104
|
|
|
86
105
|
const entry = {
|
|
87
106
|
...message,
|
|
88
107
|
at: new Date().toISOString(),
|
|
89
|
-
agent_teams:
|
|
108
|
+
agent_teams: isNative,
|
|
90
109
|
};
|
|
91
110
|
|
|
111
|
+
// When native Agent Teams is enabled, also format for native SendMessage
|
|
112
|
+
if (isNative) {
|
|
113
|
+
entry.native_format = {
|
|
114
|
+
tool: 'SendMessage',
|
|
115
|
+
to: message.to || 'team-lead',
|
|
116
|
+
content: formatForNative(message),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Always write to JSONL bus (source of truth)
|
|
92
121
|
fs.appendFileSync(logPath, JSON.stringify(entry) + '\n');
|
|
93
|
-
|
|
122
|
+
|
|
123
|
+
// Trigger rotation check (non-blocking, fail-safe)
|
|
124
|
+
try {
|
|
125
|
+
const busUtils = getBusUtils();
|
|
126
|
+
if (busUtils && busUtils.shouldRotate(logPath, 1000)) {
|
|
127
|
+
busUtils.rotateLog(logPath, { keepRecent: 100 });
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
// Rotation failure is non-critical
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { ok: true, native: isNative };
|
|
94
134
|
} catch (e) {
|
|
95
135
|
return { ok: false, error: e.message };
|
|
96
136
|
}
|
|
97
137
|
}
|
|
98
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Format a message for the native SendMessage tool.
|
|
141
|
+
* Converts AgileFlow message types into a structured content string.
|
|
142
|
+
*
|
|
143
|
+
* @param {object} message - AgileFlow message object
|
|
144
|
+
* @returns {string} Formatted content for native SendMessage
|
|
145
|
+
* @private
|
|
146
|
+
*/
|
|
147
|
+
function formatForNative(message) {
|
|
148
|
+
const parts = [];
|
|
149
|
+
if (message.type) parts.push(`[${message.type}]`);
|
|
150
|
+
if (message.from) parts.push(`from:${message.from}`);
|
|
151
|
+
if (message.task_id) parts.push(`task:${message.task_id}`);
|
|
152
|
+
if (message.message) parts.push(message.message);
|
|
153
|
+
if (message.description) parts.push(message.description);
|
|
154
|
+
if (message.status) parts.push(`status:${message.status}`);
|
|
155
|
+
return parts.join(' ');
|
|
156
|
+
}
|
|
157
|
+
|
|
99
158
|
/**
|
|
100
159
|
* Read messages from the AgileFlow bus.
|
|
101
160
|
*
|
|
@@ -188,54 +247,37 @@ function getAgentContext(rootDir, agentName) {
|
|
|
188
247
|
/**
|
|
189
248
|
* Send a task assignment message.
|
|
190
249
|
*/
|
|
191
|
-
function sendTaskAssignment(rootDir, from, to, taskId, description) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
type: 'task_assignment',
|
|
196
|
-
task_id: taskId,
|
|
197
|
-
description,
|
|
198
|
-
});
|
|
250
|
+
function sendTaskAssignment(rootDir, from, to, taskId, description, traceId) {
|
|
251
|
+
const msg = { from, to, type: 'task_assignment', task_id: taskId, description };
|
|
252
|
+
if (traceId) msg.trace_id = traceId;
|
|
253
|
+
return sendMessage(rootDir, msg);
|
|
199
254
|
}
|
|
200
255
|
|
|
201
256
|
/**
|
|
202
257
|
* Send a plan proposal message.
|
|
203
258
|
*/
|
|
204
|
-
function sendPlanProposal(rootDir, from, to, taskId, plan) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
type: 'plan_proposal',
|
|
209
|
-
task_id: taskId,
|
|
210
|
-
plan,
|
|
211
|
-
});
|
|
259
|
+
function sendPlanProposal(rootDir, from, to, taskId, plan, traceId) {
|
|
260
|
+
const msg = { from, to, type: 'plan_proposal', task_id: taskId, plan };
|
|
261
|
+
if (traceId) msg.trace_id = traceId;
|
|
262
|
+
return sendMessage(rootDir, msg);
|
|
212
263
|
}
|
|
213
264
|
|
|
214
265
|
/**
|
|
215
266
|
* Send a plan approval/rejection message.
|
|
216
267
|
*/
|
|
217
|
-
function sendPlanDecision(rootDir, from, to, taskId, approved, reason) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
type: approved ? 'plan_approved' : 'plan_rejected',
|
|
222
|
-
task_id: taskId,
|
|
223
|
-
reason,
|
|
224
|
-
});
|
|
268
|
+
function sendPlanDecision(rootDir, from, to, taskId, approved, reason, traceId) {
|
|
269
|
+
const msg = { from, to, type: approved ? 'plan_approved' : 'plan_rejected', task_id: taskId, reason };
|
|
270
|
+
if (traceId) msg.trace_id = traceId;
|
|
271
|
+
return sendMessage(rootDir, msg);
|
|
225
272
|
}
|
|
226
273
|
|
|
227
274
|
/**
|
|
228
275
|
* Send a validation result message.
|
|
229
276
|
*/
|
|
230
|
-
function sendValidationResult(rootDir, from, taskId, status, details) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
type: 'validation',
|
|
235
|
-
task_id: taskId,
|
|
236
|
-
status, // 'approved' | 'rejected'
|
|
237
|
-
details,
|
|
238
|
-
});
|
|
277
|
+
function sendValidationResult(rootDir, from, taskId, status, details, traceId) {
|
|
278
|
+
const msg = { from, to: 'team-lead', type: 'validation', task_id: taskId, status, details };
|
|
279
|
+
if (traceId) msg.trace_id = traceId;
|
|
280
|
+
return sendMessage(rootDir, msg);
|
|
239
281
|
}
|
|
240
282
|
|
|
241
283
|
// CLI entry point
|
|
@@ -297,6 +339,7 @@ module.exports = {
|
|
|
297
339
|
sendPlanDecision,
|
|
298
340
|
sendValidationResult,
|
|
299
341
|
getBusLogPath,
|
|
342
|
+
formatForNative,
|
|
300
343
|
};
|
|
301
344
|
|
|
302
345
|
if (require.main === module) {
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* migrate-ideation-index.js - Backfill ideation index from existing reports
|
|
5
5
|
*
|
|
6
|
-
* Parses all existing ideation reports (docs/08-project/ideation-*.md
|
|
6
|
+
* Parses all existing ideation reports (docs/08-project/ideation/ideation-*.md,
|
|
7
|
+
* with fallback to legacy docs/08-project/ideation-*.md) and
|
|
7
8
|
* populates the ideation index with ideas, detecting duplicates across reports
|
|
8
9
|
* and linking to stories/epics where possible.
|
|
9
10
|
*
|
|
@@ -290,19 +291,40 @@ function migrateIdeationReports(rootDir, options = {}) {
|
|
|
290
291
|
|
|
291
292
|
const index = loadResult.data;
|
|
292
293
|
|
|
293
|
-
// Find all ideation reports
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
// Find all ideation reports (new path first, then legacy fallback)
|
|
295
|
+
const newReportsDir = path.join(rootDir, 'docs/08-project/ideation');
|
|
296
|
+
const legacyReportsDir = path.join(rootDir, 'docs/08-project');
|
|
297
|
+
|
|
298
|
+
const reportFiles = [];
|
|
299
|
+
|
|
300
|
+
// Check new subdirectory first
|
|
301
|
+
if (fs.existsSync(newReportsDir)) {
|
|
302
|
+
const newFiles = fs
|
|
303
|
+
.readdirSync(newReportsDir)
|
|
304
|
+
.filter(f => f.startsWith('ideation-') && f.endsWith('.md'));
|
|
305
|
+
for (const f of newFiles) {
|
|
306
|
+
reportFiles.push({ file: f, dir: newReportsDir });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Also check legacy location for backward compatibility
|
|
311
|
+
if (fs.existsSync(legacyReportsDir)) {
|
|
312
|
+
const legacyFiles = fs
|
|
313
|
+
.readdirSync(legacyReportsDir)
|
|
314
|
+
.filter(f => f.startsWith('ideation-') && f.endsWith('.md'));
|
|
315
|
+
for (const f of legacyFiles) {
|
|
316
|
+
// Avoid duplicates if same file exists in both locations
|
|
317
|
+
if (!reportFiles.some(r => r.file === f)) {
|
|
318
|
+
reportFiles.push({ file: f, dir: legacyReportsDir });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
297
321
|
}
|
|
298
322
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
.filter(f => f.startsWith('ideation-') && f.endsWith('.md'))
|
|
302
|
-
.sort(); // Sort chronologically
|
|
323
|
+
// Sort chronologically by filename
|
|
324
|
+
reportFiles.sort((a, b) => a.file.localeCompare(b.file));
|
|
303
325
|
|
|
304
326
|
if (reportFiles.length === 0) {
|
|
305
|
-
console.log('No ideation reports found in docs/08-project/');
|
|
327
|
+
console.log('No ideation reports found in docs/08-project/ideation/ or docs/08-project/');
|
|
306
328
|
return { ok: true, stats: { reportsProcessed: 0, ideasAdded: 0, duplicates: 0 } };
|
|
307
329
|
}
|
|
308
330
|
|
|
@@ -315,7 +337,7 @@ function migrateIdeationReports(rootDir, options = {}) {
|
|
|
315
337
|
errors: 0,
|
|
316
338
|
};
|
|
317
339
|
|
|
318
|
-
for (const reportFile of reportFiles) {
|
|
340
|
+
for (const { file: reportFile, dir: reportsDir } of reportFiles) {
|
|
319
341
|
const reportPath = path.join(reportsDir, reportFile);
|
|
320
342
|
console.log(`Processing: ${reportFile}`);
|
|
321
343
|
|
|
@@ -499,9 +521,10 @@ Options:
|
|
|
499
521
|
--help Show this help
|
|
500
522
|
|
|
501
523
|
Description:
|
|
502
|
-
Parses all ideation reports in docs/08-project/ideation-*.md and
|
|
503
|
-
|
|
504
|
-
|
|
524
|
+
Parses all ideation reports in docs/08-project/ideation/ideation-*.md (and
|
|
525
|
+
legacy docs/08-project/ideation-*.md) and populates the ideation index at
|
|
526
|
+
docs/00-meta/ideation-index.json with ideas, detecting duplicates across
|
|
527
|
+
reports and linking to epics where possible.
|
|
505
528
|
`);
|
|
506
529
|
process.exit(0);
|
|
507
530
|
}
|