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.
Files changed (65) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/lib/cache-provider.js +155 -0
  3. package/lib/codebase-indexer.js +1 -1
  4. package/lib/content-sanitizer.js +1 -0
  5. package/lib/dashboard-protocol.js +25 -0
  6. package/lib/dashboard-server.js +184 -133
  7. package/lib/errors.js +18 -0
  8. package/lib/file-cache.js +1 -1
  9. package/lib/flag-detection.js +11 -20
  10. package/lib/git-operations.js +15 -33
  11. package/lib/merge-operations.js +40 -34
  12. package/lib/process-executor.js +199 -0
  13. package/lib/registry-cache.js +13 -47
  14. package/lib/skill-loader.js +206 -0
  15. package/lib/smart-json-file.js +2 -4
  16. package/package.json +1 -1
  17. package/scripts/agileflow-configure.js +13 -12
  18. package/scripts/agileflow-statusline.sh +30 -0
  19. package/scripts/agileflow-welcome.js +181 -212
  20. package/scripts/auto-self-improve.js +3 -3
  21. package/scripts/claude-smart.sh +67 -0
  22. package/scripts/claude-tmux.sh +248 -161
  23. package/scripts/damage-control-multi-agent.js +227 -0
  24. package/scripts/lib/bus-utils.js +471 -0
  25. package/scripts/lib/configure-detect.js +5 -6
  26. package/scripts/lib/configure-features.js +44 -0
  27. package/scripts/lib/configure-repair.js +5 -6
  28. package/scripts/lib/configure-utils.js +2 -3
  29. package/scripts/lib/context-formatter.js +87 -8
  30. package/scripts/lib/damage-control-utils.js +37 -3
  31. package/scripts/lib/file-lock.js +392 -0
  32. package/scripts/lib/ideation-index.js +2 -5
  33. package/scripts/lib/lifecycle-detector.js +123 -0
  34. package/scripts/lib/process-cleanup.js +55 -81
  35. package/scripts/lib/scale-detector.js +357 -0
  36. package/scripts/lib/signal-detectors.js +779 -0
  37. package/scripts/lib/story-state-machine.js +1 -1
  38. package/scripts/lib/sync-ideation-status.js +2 -3
  39. package/scripts/lib/task-registry.js +7 -1
  40. package/scripts/lib/team-events.js +357 -0
  41. package/scripts/messaging-bridge.js +79 -36
  42. package/scripts/migrate-ideation-index.js +37 -14
  43. package/scripts/obtain-context.js +37 -19
  44. package/scripts/ralph-loop.js +3 -4
  45. package/scripts/smart-detect.js +390 -0
  46. package/scripts/team-manager.js +174 -30
  47. package/src/core/commands/audit.md +13 -11
  48. package/src/core/commands/babysit.md +162 -115
  49. package/src/core/commands/changelog.md +21 -4
  50. package/src/core/commands/configure.md +105 -2
  51. package/src/core/commands/debt.md +12 -2
  52. package/src/core/commands/feedback.md +7 -6
  53. package/src/core/commands/ideate/history.md +1 -1
  54. package/src/core/commands/ideate/new.md +5 -5
  55. package/src/core/commands/logic/audit.md +2 -2
  56. package/src/core/commands/pr.md +7 -6
  57. package/src/core/commands/research/analyze.md +28 -20
  58. package/src/core/commands/research/ask.md +43 -0
  59. package/src/core/commands/research/import.md +29 -21
  60. package/src/core/commands/research/list.md +8 -7
  61. package/src/core/commands/research/synthesize.md +356 -20
  62. package/src/core/commands/research/view.md +8 -5
  63. package/src/core/commands/review.md +24 -6
  64. package/src/core/commands/skill/create.md +34 -0
  65. 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 = ['completed', 'done', 'archived'];
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
- try {
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
- const fd = fs.openSync(this.lockPath, 'wx');
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: getFeatureFlags().isAgentTeamsEnabled({ rootDir }),
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
- return { ok: true };
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
- return sendMessage(rootDir, {
193
- from,
194
- to,
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
- return sendMessage(rootDir, {
206
- from,
207
- to,
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
- return sendMessage(rootDir, {
219
- from,
220
- to,
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
- return sendMessage(rootDir, {
232
- from,
233
- to: 'team-lead',
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) and
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 reportsDir = path.join(rootDir, 'docs/08-project');
295
- if (!fs.existsSync(reportsDir)) {
296
- return { ok: false, error: `Reports directory not found: ${reportsDir}` };
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
- const reportFiles = fs
300
- .readdirSync(reportsDir)
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 populates
503
- the ideation index at docs/00-meta/ideation-index.json with ideas, detecting
504
- duplicates across reports and linking to epics where possible.
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
  }