@ginkoai/cli 2.0.6 → 2.1.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 (90) hide show
  1. package/dist/commands/epic/index.d.ts +29 -0
  2. package/dist/commands/epic/index.d.ts.map +1 -0
  3. package/dist/commands/epic/index.js +94 -0
  4. package/dist/commands/epic/index.js.map +1 -0
  5. package/dist/commands/epic/status.d.ts +40 -0
  6. package/dist/commands/epic/status.d.ts.map +1 -0
  7. package/dist/commands/epic/status.js +244 -0
  8. package/dist/commands/epic/status.js.map +1 -0
  9. package/dist/commands/graph/api-client.d.ts +209 -0
  10. package/dist/commands/graph/api-client.d.ts.map +1 -1
  11. package/dist/commands/graph/api-client.js +125 -0
  12. package/dist/commands/graph/api-client.js.map +1 -1
  13. package/dist/commands/graph/load.d.ts.map +1 -1
  14. package/dist/commands/graph/load.js +40 -2
  15. package/dist/commands/graph/load.js.map +1 -1
  16. package/dist/commands/migrate/index.d.ts +27 -0
  17. package/dist/commands/migrate/index.d.ts.map +1 -0
  18. package/dist/commands/migrate/index.js +76 -0
  19. package/dist/commands/migrate/index.js.map +1 -0
  20. package/dist/commands/migrate/status-migration.d.ts +58 -0
  21. package/dist/commands/migrate/status-migration.d.ts.map +1 -0
  22. package/dist/commands/migrate/status-migration.js +323 -0
  23. package/dist/commands/migrate/status-migration.js.map +1 -0
  24. package/dist/commands/sprint/index.d.ts.map +1 -1
  25. package/dist/commands/sprint/index.js +4 -0
  26. package/dist/commands/sprint/index.js.map +1 -1
  27. package/dist/commands/sprint/status.d.ts +42 -0
  28. package/dist/commands/sprint/status.d.ts.map +1 -0
  29. package/dist/commands/sprint/status.js +278 -0
  30. package/dist/commands/sprint/status.js.map +1 -0
  31. package/dist/commands/start/start-reflection.d.ts +39 -0
  32. package/dist/commands/start/start-reflection.d.ts.map +1 -1
  33. package/dist/commands/start/start-reflection.js +292 -70
  34. package/dist/commands/start/start-reflection.js.map +1 -1
  35. package/dist/commands/sync/sprint-syncer.d.ts +19 -12
  36. package/dist/commands/sync/sprint-syncer.d.ts.map +1 -1
  37. package/dist/commands/sync/sprint-syncer.js +58 -140
  38. package/dist/commands/sync/sprint-syncer.js.map +1 -1
  39. package/dist/commands/sync/sync-command.d.ts.map +1 -1
  40. package/dist/commands/sync/sync-command.js +6 -18
  41. package/dist/commands/sync/sync-command.js.map +1 -1
  42. package/dist/commands/task/index.d.ts +25 -0
  43. package/dist/commands/task/index.d.ts.map +1 -0
  44. package/dist/commands/task/index.js +100 -0
  45. package/dist/commands/task/index.js.map +1 -0
  46. package/dist/commands/task/status.d.ts +43 -0
  47. package/dist/commands/task/status.d.ts.map +1 -0
  48. package/dist/commands/task/status.js +301 -0
  49. package/dist/commands/task/status.js.map +1 -0
  50. package/dist/index.js +11 -29
  51. package/dist/index.js.map +1 -1
  52. package/dist/lib/context-loader-events.d.ts.map +1 -1
  53. package/dist/lib/context-loader-events.js +7 -26
  54. package/dist/lib/context-loader-events.js.map +1 -1
  55. package/dist/lib/output-formatter.d.ts +8 -2
  56. package/dist/lib/output-formatter.d.ts.map +1 -1
  57. package/dist/lib/output-formatter.js +98 -18
  58. package/dist/lib/output-formatter.js.map +1 -1
  59. package/dist/lib/pending-updates.d.ts +148 -0
  60. package/dist/lib/pending-updates.d.ts.map +1 -0
  61. package/dist/lib/pending-updates.js +301 -0
  62. package/dist/lib/pending-updates.js.map +1 -0
  63. package/dist/lib/sprint-loader.d.ts +86 -14
  64. package/dist/lib/sprint-loader.d.ts.map +1 -1
  65. package/dist/lib/sprint-loader.js +293 -98
  66. package/dist/lib/sprint-loader.js.map +1 -1
  67. package/dist/lib/state-cache.d.ts +142 -0
  68. package/dist/lib/state-cache.d.ts.map +1 -0
  69. package/dist/lib/state-cache.js +259 -0
  70. package/dist/lib/state-cache.js.map +1 -0
  71. package/dist/lib/task-graph-sync.d.ts +105 -0
  72. package/dist/lib/task-graph-sync.d.ts.map +1 -0
  73. package/dist/lib/task-graph-sync.js +178 -0
  74. package/dist/lib/task-graph-sync.js.map +1 -0
  75. package/dist/lib/task-parser.d.ts +107 -0
  76. package/dist/lib/task-parser.d.ts.map +1 -0
  77. package/dist/lib/task-parser.js +384 -0
  78. package/dist/lib/task-parser.js.map +1 -0
  79. package/dist/templates/ai-instructions-template.d.ts.map +1 -1
  80. package/dist/templates/ai-instructions-template.js +7 -5
  81. package/dist/templates/ai-instructions-template.js.map +1 -1
  82. package/dist/templates/epic-template.md +0 -2
  83. package/dist/types/config.d.ts.map +1 -1
  84. package/dist/types/config.js +7 -5
  85. package/dist/types/config.js.map +1 -1
  86. package/dist/utils/synthesis.d.ts +4 -0
  87. package/dist/utils/synthesis.d.ts.map +1 -1
  88. package/dist/utils/synthesis.js +12 -18
  89. package/dist/utils/synthesis.js.map +1 -1
  90. package/package.json +1 -1
@@ -21,6 +21,7 @@ import { SessionSynthesizer } from '../../utils/synthesis.js';
21
21
  import { loadContextStrategic, formatContextSummary } from '../../utils/context-loader.js';
22
22
  import { initializeQueue } from '../../lib/event-queue.js';
23
23
  import { loadSprintChecklist, formatSprintChecklist, formatCurrentTaskDetails, detectSprintProgression } from '../../lib/sprint-loader.js';
24
+ import { loadStateCache, saveStateCache, checkCacheStaleness } from '../../lib/state-cache.js';
24
25
  import { formatHumanOutput, formatVerboseOutput, formatAIContextJSONL, formatTableOutput, formatEpicComplete, formatSprintProgressionPrompt } from '../../lib/output-formatter.js';
25
26
  import { getUserCurrentSprint, setUserCurrentSprint, createAssignmentFromFile, getSprintFileFromAssignment } from '../../lib/user-sprint.js';
26
27
  import { GraphApiClient } from '../graph/api-client.js';
@@ -156,11 +157,222 @@ export class StartReflectionCommand extends ReflectionCommand {
156
157
  source: 'graph', // Mark source for debugging
157
158
  };
158
159
  }
160
+ /**
161
+ * Load sprint state from graph API with cache fallback (EPIC-015 Sprint 2 Task 3)
162
+ *
163
+ * Data flow:
164
+ * 1. Try to fetch from graph API first
165
+ * 2. On success: save to cache for offline use
166
+ * 3. On failure: load from cache
167
+ *
168
+ * @param graphId - The graph namespace ID
169
+ * @returns Sprint data and source indicator ('graph' | 'cache' | null)
170
+ */
171
+ async loadSprintStateFromGraph(graphId) {
172
+ const client = new GraphApiClient();
173
+ try {
174
+ // Try to fetch from graph API first
175
+ const graphResponse = await client.getActiveSprint(graphId);
176
+ // Convert API response to cache format
177
+ const activeSprintData = {
178
+ sprintId: graphResponse.sprint.id,
179
+ sprintName: graphResponse.sprint.name,
180
+ epicId: graphResponse.sprint.id.split('_s')[0] || 'unknown', // Extract epic from e011_s01
181
+ progress: {
182
+ completed: graphResponse.stats.completedTasks,
183
+ total: graphResponse.stats.totalTasks,
184
+ percentage: graphResponse.stats.progressPercentage,
185
+ },
186
+ currentTask: graphResponse.nextTask ? {
187
+ taskId: graphResponse.nextTask.id,
188
+ taskName: graphResponse.nextTask.title,
189
+ status: this.mapGraphStatusToTaskStatus(graphResponse.nextTask.status),
190
+ } : undefined,
191
+ nextTask: graphResponse.nextTask ? {
192
+ taskId: graphResponse.nextTask.id,
193
+ taskName: graphResponse.nextTask.title,
194
+ } : undefined,
195
+ };
196
+ // Save to cache for offline use
197
+ await saveStateCache(activeSprintData, graphId);
198
+ return {
199
+ data: activeSprintData,
200
+ source: 'graph',
201
+ };
202
+ }
203
+ catch (error) {
204
+ // Graph unavailable - try to load from cache
205
+ const cache = await loadStateCache();
206
+ if (cache && cache.graph_id === graphId) {
207
+ const staleness = checkCacheStaleness(cache);
208
+ return {
209
+ data: cache.active_sprint,
210
+ source: 'cache',
211
+ staleness,
212
+ };
213
+ }
214
+ // No cache available
215
+ return {
216
+ data: null,
217
+ source: 'cache',
218
+ };
219
+ }
220
+ }
221
+ /**
222
+ * Load sprint data from cache only (for offline fallback)
223
+ */
224
+ async loadFromCacheOnly(graphId) {
225
+ const cache = await loadStateCache();
226
+ if (cache && cache.graph_id === graphId) {
227
+ const staleness = checkCacheStaleness(cache);
228
+ return {
229
+ data: cache.active_sprint,
230
+ staleness,
231
+ };
232
+ }
233
+ return { data: null };
234
+ }
235
+ /**
236
+ * Convert cached ActiveSprintData to SprintChecklist format
237
+ */
238
+ convertCachedDataToChecklist(cachedData) {
239
+ // Build minimal checklist from cached data
240
+ const tasks = [];
241
+ // If we have a current task, add it
242
+ if (cachedData.currentTask) {
243
+ tasks.push({
244
+ id: cachedData.currentTask.taskId,
245
+ title: cachedData.currentTask.taskName,
246
+ state: this.mapStatusToTaskState(cachedData.currentTask.status || 'todo'),
247
+ });
248
+ }
249
+ const completed = cachedData.progress?.completed || 0;
250
+ const total = cachedData.progress?.total || tasks.length;
251
+ return {
252
+ name: cachedData.sprintName || cachedData.sprintId,
253
+ file: '', // No file when loading from cache
254
+ tasks,
255
+ progress: {
256
+ complete: completed,
257
+ inProgress: cachedData.currentTask ? 1 : 0,
258
+ paused: 0,
259
+ todo: Math.max(0, total - completed - (cachedData.currentTask ? 1 : 0)),
260
+ total,
261
+ },
262
+ currentTask: cachedData.currentTask ? {
263
+ id: cachedData.currentTask.taskId,
264
+ title: cachedData.currentTask.taskName,
265
+ state: this.mapStatusToTaskState(cachedData.currentTask.status || 'todo'),
266
+ } : undefined,
267
+ recentCompletions: [],
268
+ };
269
+ }
270
+ /**
271
+ * Map graph API status string to TaskStatus enum
272
+ */
273
+ mapGraphStatusToTaskStatus(status) {
274
+ switch (status?.toLowerCase()) {
275
+ case 'complete':
276
+ case 'completed':
277
+ return 'completed';
278
+ case 'in_progress':
279
+ case 'in-progress':
280
+ return 'in_progress';
281
+ default:
282
+ return 'pending';
283
+ }
284
+ }
285
+ /**
286
+ * Map graph status to TaskState enum for SprintChecklist
287
+ */
288
+ mapStatusToTaskState(status) {
289
+ switch (status?.toLowerCase()) {
290
+ case 'complete':
291
+ case 'completed':
292
+ return 'complete';
293
+ case 'in_progress':
294
+ case 'in-progress':
295
+ return 'in_progress';
296
+ case 'paused':
297
+ case 'sleeping':
298
+ return 'paused';
299
+ case 'blocked':
300
+ return 'in_progress'; // Treat blocked as in_progress for display
301
+ default:
302
+ return 'todo';
303
+ }
304
+ }
305
+ /**
306
+ * Merge graph status with local file content (EPIC-015 Sprint 2 Task 3)
307
+ *
308
+ * Graph API is authoritative for STATUS (progress, task states).
309
+ * Local file provides CONTENT (descriptions, acceptance criteria, ADRs).
310
+ *
311
+ * @param graphData - Status data from graph API (or cache)
312
+ * @param fileContent - Content from local sprint file
313
+ * @returns Merged SprintChecklist with graph status + file content
314
+ */
315
+ mergeGraphStatusWithContent(graphData, fileContent, graphTasks) {
316
+ // Build task list: merge graph status with file content
317
+ const tasks = fileContent.tasks.map((fileTask) => {
318
+ // Find matching task in graph data
319
+ const graphTask = graphTasks?.find(gt => gt.id === fileTask.id);
320
+ const status = graphTask?.status || 'not_started';
321
+ return {
322
+ id: fileTask.id,
323
+ title: fileTask.title,
324
+ state: this.mapStatusToTaskState(status),
325
+ files: fileTask.files || graphTask?.files || [],
326
+ effort: fileTask.effort,
327
+ priority: fileTask.priority || graphTask?.priority,
328
+ relatedADRs: fileTask.relatedADRs,
329
+ relatedPatterns: fileTask.relatedPatterns,
330
+ relatedGotchas: fileTask.relatedGotchas,
331
+ acceptanceCriteria: fileTask.acceptanceCriteria,
332
+ dependsOn: fileTask.dependsOn,
333
+ };
334
+ });
335
+ // Calculate progress from graph data
336
+ const progress = {
337
+ complete: graphData.progress.completed,
338
+ inProgress: tasks.filter(t => t.state === 'in_progress').length,
339
+ paused: tasks.filter(t => t.state === 'paused').length,
340
+ todo: graphData.progress.total - graphData.progress.completed - tasks.filter(t => t.state === 'in_progress').length,
341
+ total: graphData.progress.total,
342
+ };
343
+ // Determine current task (first in_progress or first todo)
344
+ const currentTask = tasks.find(t => t.state === 'in_progress') ||
345
+ tasks.find(t => t.state === 'todo');
346
+ // Recent completions (completed tasks from the end of list)
347
+ const recentCompletions = tasks
348
+ .filter(t => t.state === 'complete')
349
+ .slice(-3);
350
+ return {
351
+ name: graphData.sprintName,
352
+ file: fileContent.file,
353
+ progress,
354
+ tasks,
355
+ currentTask,
356
+ recentCompletions,
357
+ dependencyWarnings: fileContent.dependencyWarnings,
358
+ };
359
+ }
159
360
  /**
160
361
  * Override execute to process handoff and display session info
161
362
  */
162
363
  async execute(intent, options = {}) {
163
- const spinner = ora('Initializing session...').start();
364
+ // Disable spinner in non-TTY mode (e.g., Claude Code) to prevent output pollution
365
+ // The table output will be the only visible output
366
+ const isTTY = process.stdout.isTTY === true;
367
+ const spinner = ora({
368
+ text: 'Initializing session...',
369
+ isEnabled: isTTY,
370
+ isSilent: !isTTY, // Completely silence output in non-TTY mode
371
+ });
372
+ // Only start spinner animation in TTY mode
373
+ if (isTTY) {
374
+ spinner.start();
375
+ }
164
376
  try {
165
377
  // 1. Parse intent
166
378
  const parsedIntent = this.parseIntent(intent);
@@ -188,24 +400,27 @@ export class StartReflectionCommand extends ReflectionCommand {
188
400
  const userSlug = userEmail.replace('@', '-at-').replace(/\./g, '-');
189
401
  const sessionDir = path.join(ginkoDir, 'sessions', userSlug);
190
402
  const projectRoot = await getProjectRoot();
191
- // Load sprint checklist for session context
192
- // Strategy: User sprint first, then local/graph
193
- // EPIC-012: Per-user sprint tracking enables multiple users on different sprints
403
+ // Load sprint checklist for session context (EPIC-015 Sprint 2 Task 3)
404
+ // NEW DATA FLOW: Graph API is authoritative for STATUS, file provides CONTENT
405
+ // 1. Fetch status from graph API (with cache fallback)
406
+ // 2. Load content from local sprint file
407
+ // 3. Merge: graph status + file content
194
408
  let sprintChecklist = null;
195
409
  let sprintSource = 'local';
410
+ let isOffline = false;
411
+ let cacheAge;
412
+ let cacheStaleness;
196
413
  // First, check if user has a specific sprint assignment
414
+ // EPIC-012: Per-user sprint tracking enables multiple users on different sprints
197
415
  const userSprint = await getUserCurrentSprint();
198
416
  let userSprintLoaded = false;
417
+ let sprintFilePath;
199
418
  if (userSprint) {
200
419
  try {
201
420
  const userSprintFile = await getSprintFileFromAssignment(userSprint);
202
421
  if (await fs.pathExists(userSprintFile)) {
203
- const userSprintChecklist = await loadSprintChecklist(projectRoot, userSprintFile);
204
- if (userSprintChecklist) {
205
- sprintChecklist = userSprintChecklist;
206
- sprintSource = 'user';
207
- userSprintLoaded = true;
208
- }
422
+ sprintFilePath = userSprintFile;
423
+ userSprintLoaded = true;
209
424
  }
210
425
  }
211
426
  catch {
@@ -213,72 +428,74 @@ export class StartReflectionCommand extends ReflectionCommand {
213
428
  // We'll fall back to global sprint
214
429
  }
215
430
  }
216
- // If no user sprint, load global sprint (CURRENT-SPRINT.md)
217
- let localSprint = null;
218
- if (!userSprintLoaded) {
219
- localSprint = await loadSprintChecklist(projectRoot);
220
- }
221
- // Only check graph/local if user sprint wasn't already loaded
222
- if (!userSprintLoaded) {
223
- // Try to load from graph if authenticated
224
- let graphSprint = null;
225
- if (await isAuthenticated() && await isGraphInitialized()) {
226
- try {
227
- spinner.text = 'Checking graph for sprint updates...';
228
- const graphId = await getGraphId();
229
- if (graphId) {
230
- const client = new GraphApiClient();
231
- const graphResponse = await client.getActiveSprint(graphId);
232
- graphSprint = this.convertGraphSprintToChecklist(graphResponse);
233
- }
234
- }
235
- catch (error) {
236
- // Graph unavailable - use local only
237
- spinner.text = 'Graph unavailable, using local sprint file';
238
- }
431
+ // Load sprint with graph-first status (EPIC-015 Sprint 2 Task 3)
432
+ const graphId = await isGraphInitialized() ? await getGraphId() : null;
433
+ const isGraphReady = await isAuthenticated() && graphId;
434
+ if (isGraphReady && graphId) {
435
+ // Graph-only approach: fetch state directly from graph API (EPIC-015 Sprint 3)
436
+ // No local file loading - graph is authoritative for STATE
437
+ spinner.text = 'Fetching sprint from graph...';
438
+ const client = new GraphApiClient();
439
+ try {
440
+ const graphResponse = await client.getActiveSprint(graphId);
441
+ sprintChecklist = this.convertGraphSprintToChecklist(graphResponse);
442
+ sprintSource = 'graph';
443
+ // Save to cache for offline fallback
444
+ const activeSprintData = {
445
+ sprintId: graphResponse.sprint.id,
446
+ sprintName: graphResponse.sprint.name,
447
+ epicId: graphResponse.sprint.id.split('_s')[0] || 'unknown',
448
+ progress: {
449
+ completed: graphResponse.stats.completedTasks,
450
+ total: graphResponse.stats.totalTasks,
451
+ percentage: graphResponse.stats.progressPercentage,
452
+ },
453
+ currentTask: graphResponse.nextTask ? {
454
+ taskId: graphResponse.nextTask.id,
455
+ taskName: graphResponse.nextTask.title,
456
+ status: this.mapGraphStatusToTaskStatus(graphResponse.nextTask.status),
457
+ } : undefined,
458
+ nextTask: graphResponse.nextTask ? {
459
+ taskId: graphResponse.nextTask.id,
460
+ taskName: graphResponse.nextTask.title,
461
+ } : undefined,
462
+ };
463
+ await saveStateCache(activeSprintData, graphId);
239
464
  }
240
- // Compare and choose the more complete/current sprint
241
- // Priority: Local file is source of truth - it represents what the user is actively working on
242
- // Graph data may be stale (e.g., still showing Sprint 1 when user moved to Sprint 2)
243
- if (localSprint && graphSprint) {
244
- // If the sprints are different (different names), always use local
245
- // This handles the case where graph hasn't been synced with new sprint
246
- const localName = localSprint.name?.toLowerCase() || '';
247
- const graphName = graphSprint.name?.toLowerCase() || '';
248
- const sprintsAreDifferent = localName !== graphName &&
249
- !localName.includes(graphName) &&
250
- !graphName.includes(localName);
251
- if (sprintsAreDifferent) {
252
- // Local file is the source of truth for which sprint we're on
253
- sprintChecklist = localSprint;
254
- sprintSource = 'local';
465
+ catch (error) {
466
+ // Graph unavailable - try cache fallback
467
+ const { data: cachedData, staleness } = await this.loadFromCacheOnly(graphId);
468
+ if (cachedData) {
469
+ // Use cached data
470
+ sprintChecklist = this.convertCachedDataToChecklist(cachedData);
471
+ sprintSource = 'cache';
472
+ isOffline = true;
473
+ cacheAge = staleness?.ageHuman;
474
+ cacheStaleness = staleness?.level;
475
+ if (staleness?.showWarning) {
476
+ spinner.text = `Using cached sprint (${staleness.ageHuman})`;
477
+ }
255
478
  }
256
479
  else {
257
- // Same sprint - prefer local if it has more tasks OR higher completion count
258
- // This handles case where graph has stale/test data
259
- const localTotal = localSprint.progress?.total || 0;
260
- const graphTotal = graphSprint.progress?.total || 0;
261
- const localProgress = localSprint.progress?.complete || 0;
262
- const graphProgress = graphSprint.progress?.complete || 0;
263
- if (localTotal > graphTotal || localProgress > graphProgress) {
264
- sprintChecklist = localSprint;
480
+ // No cache - fall back to local file (offline fallback only)
481
+ spinner.text = 'Graph unavailable, using local sprint file';
482
+ const localChecklist = await loadSprintChecklist(projectRoot, sprintFilePath);
483
+ if (localChecklist) {
484
+ sprintChecklist = localChecklist;
265
485
  sprintSource = 'local';
266
- }
267
- else {
268
- sprintChecklist = graphSprint;
269
- sprintSource = 'graph';
486
+ isOffline = true;
270
487
  }
271
488
  }
272
489
  }
273
- else if (localSprint) {
274
- sprintChecklist = localSprint;
275
- sprintSource = 'local';
276
- }
277
- else if (graphSprint) {
278
- sprintChecklist = graphSprint;
279
- sprintSource = 'graph';
490
+ }
491
+ else {
492
+ // Graph not initialized - use local file only
493
+ const localChecklist = await loadSprintChecklist(projectRoot, sprintFilePath);
494
+ if (localChecklist) {
495
+ sprintChecklist = localChecklist;
496
+ sprintSource = userSprintLoaded ? 'user' : 'local';
280
497
  }
281
- } // end if (!userSprintLoaded)
498
+ }
282
499
  // Load session log content before archiving
283
500
  const previousSessionLog = await SessionLogManager.loadSessionLog(sessionDir);
284
501
  const hasLog = previousSessionLog.length > 100; // Non-empty log
@@ -398,7 +615,8 @@ export class StartReflectionCommand extends ReflectionCommand {
398
615
  // Session logging status shown in table if needed
399
616
  // 11. Build AI context for dual output system (TASK-11)
400
617
  // Now async due to graph API enrichment (EPIC-002 Sprint 3 completion)
401
- const aiContext = await this.buildAIContext(context, activeSynthesis, strategyContext, eventContext, sprintChecklist, isFirstTimeMember);
618
+ // EPIC-015 Sprint 2: Include offline status for display
619
+ const aiContext = await this.buildAIContext(context, activeSynthesis, strategyContext, eventContext, sprintChecklist, isFirstTimeMember, { isOffline, cacheAge, cacheStaleness });
402
620
  // Store AI context for MCP/external access
403
621
  await this.storeAIContext(aiContext, sessionDir);
404
622
  // 12. Check sprint progression and epic completion (EPIC-012 Sprint 1)
@@ -1252,7 +1470,7 @@ Example output structure:
1252
1470
  * Creates a rich, structured context object that AI partners can parse.
1253
1471
  * This is the "AI UX" side of the dual output system.
1254
1472
  */
1255
- async buildAIContext(context, synthesis, strategyContext, eventContext, sprintChecklist, isFirstTimeMember = false) {
1473
+ async buildAIContext(context, synthesis, strategyContext, eventContext, sprintChecklist, isFirstTimeMember = false, offlineStatus) {
1256
1474
  const workMode = context.workMode || 'Think & Build';
1257
1475
  // Build sprint object
1258
1476
  let sprint;
@@ -1335,6 +1553,10 @@ Example output structure:
1335
1553
  flowState: synthesis?.flowState?.energy || 'neutral',
1336
1554
  workMode: workMode,
1337
1555
  isFirstTimeMember,
1556
+ // EPIC-015 Sprint 2: Offline status indicators
1557
+ isOffline: offlineStatus?.isOffline,
1558
+ cacheAge: offlineStatus?.cacheAge,
1559
+ cacheStaleness: offlineStatus?.cacheStaleness,
1338
1560
  },
1339
1561
  charter: eventContext?.strategicContext?.charter,
1340
1562
  teamActivity: eventContext?.strategicContext?.teamActivity ? {