@ginkoai/cli 2.0.5 → 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 (91) 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 +311 -91
  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 +12 -29
  51. package/dist/index.js.map +1 -1
  52. package/dist/lib/context-loader-events.d.ts +1 -0
  53. package/dist/lib/context-loader-events.d.ts.map +1 -1
  54. package/dist/lib/context-loader-events.js +28 -41
  55. package/dist/lib/context-loader-events.js.map +1 -1
  56. package/dist/lib/output-formatter.d.ts +12 -4
  57. package/dist/lib/output-formatter.d.ts.map +1 -1
  58. package/dist/lib/output-formatter.js +186 -14
  59. package/dist/lib/output-formatter.js.map +1 -1
  60. package/dist/lib/pending-updates.d.ts +148 -0
  61. package/dist/lib/pending-updates.d.ts.map +1 -0
  62. package/dist/lib/pending-updates.js +301 -0
  63. package/dist/lib/pending-updates.js.map +1 -0
  64. package/dist/lib/sprint-loader.d.ts +86 -14
  65. package/dist/lib/sprint-loader.d.ts.map +1 -1
  66. package/dist/lib/sprint-loader.js +293 -98
  67. package/dist/lib/sprint-loader.js.map +1 -1
  68. package/dist/lib/state-cache.d.ts +142 -0
  69. package/dist/lib/state-cache.d.ts.map +1 -0
  70. package/dist/lib/state-cache.js +259 -0
  71. package/dist/lib/state-cache.js.map +1 -0
  72. package/dist/lib/task-graph-sync.d.ts +105 -0
  73. package/dist/lib/task-graph-sync.d.ts.map +1 -0
  74. package/dist/lib/task-graph-sync.js +178 -0
  75. package/dist/lib/task-graph-sync.js.map +1 -0
  76. package/dist/lib/task-parser.d.ts +107 -0
  77. package/dist/lib/task-parser.d.ts.map +1 -0
  78. package/dist/lib/task-parser.js +384 -0
  79. package/dist/lib/task-parser.js.map +1 -0
  80. package/dist/templates/ai-instructions-template.d.ts.map +1 -1
  81. package/dist/templates/ai-instructions-template.js +7 -5
  82. package/dist/templates/ai-instructions-template.js.map +1 -1
  83. package/dist/templates/epic-template.md +0 -2
  84. package/dist/types/config.d.ts.map +1 -1
  85. package/dist/types/config.js +7 -5
  86. package/dist/types/config.js.map +1 -1
  87. package/dist/utils/synthesis.d.ts +4 -0
  88. package/dist/utils/synthesis.d.ts.map +1 -1
  89. package/dist/utils/synthesis.js +12 -18
  90. package/dist/utils/synthesis.js.map +1 -1
  91. package/package.json +1 -1
@@ -20,8 +20,8 @@ import { SessionLogManager } from '../../core/session-log-manager.js';
20
20
  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
- import { formatContextSummary as formatEventContextSummary } from '../../lib/context-loader-events.js';
24
23
  import { loadSprintChecklist, formatSprintChecklist, formatCurrentTaskDetails, detectSprintProgression } from '../../lib/sprint-loader.js';
24
+ import { loadStateCache, saveStateCache, checkCacheStaleness } from '../../lib/state-cache.js';
25
25
  import { formatHumanOutput, formatVerboseOutput, formatAIContextJSONL, formatTableOutput, formatEpicComplete, formatSprintProgressionPrompt } from '../../lib/output-formatter.js';
26
26
  import { getUserCurrentSprint, setUserCurrentSprint, createAssignmentFromFile, getSprintFileFromAssignment } from '../../lib/user-sprint.js';
27
27
  import { GraphApiClient } from '../graph/api-client.js';
@@ -157,11 +157,222 @@ export class StartReflectionCommand extends ReflectionCommand {
157
157
  source: 'graph', // Mark source for debugging
158
158
  };
159
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
+ }
160
360
  /**
161
361
  * Override execute to process handoff and display session info
162
362
  */
163
363
  async execute(intent, options = {}) {
164
- 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
+ }
165
376
  try {
166
377
  // 1. Parse intent
167
378
  const parsedIntent = this.parseIntent(intent);
@@ -174,11 +385,10 @@ export class StartReflectionCommand extends ReflectionCommand {
174
385
  maxBatchSize: 20, // 20 events max
175
386
  silent: true // Suppress logs for clean table output
176
387
  });
177
- spinner.succeed(chalk.dim('Event sync queue started'));
388
+ // Don't print - table will be the only output
178
389
  }
179
390
  catch (error) {
180
- // Non-critical - continue without sync
181
- spinner.warn(chalk.yellow('Event sync queue unavailable (offline mode)'));
391
+ // Non-critical - continue without sync (silent)
182
392
  }
183
393
  // 3. Gather context (including handoff)
184
394
  const context = await this.gatherContext(parsedIntent);
@@ -190,24 +400,27 @@ export class StartReflectionCommand extends ReflectionCommand {
190
400
  const userSlug = userEmail.replace('@', '-at-').replace(/\./g, '-');
191
401
  const sessionDir = path.join(ginkoDir, 'sessions', userSlug);
192
402
  const projectRoot = await getProjectRoot();
193
- // Load sprint checklist for session context
194
- // Strategy: User sprint first, then local/graph
195
- // 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
196
408
  let sprintChecklist = null;
197
409
  let sprintSource = 'local';
410
+ let isOffline = false;
411
+ let cacheAge;
412
+ let cacheStaleness;
198
413
  // First, check if user has a specific sprint assignment
414
+ // EPIC-012: Per-user sprint tracking enables multiple users on different sprints
199
415
  const userSprint = await getUserCurrentSprint();
200
416
  let userSprintLoaded = false;
417
+ let sprintFilePath;
201
418
  if (userSprint) {
202
419
  try {
203
420
  const userSprintFile = await getSprintFileFromAssignment(userSprint);
204
421
  if (await fs.pathExists(userSprintFile)) {
205
- const userSprintChecklist = await loadSprintChecklist(projectRoot, userSprintFile);
206
- if (userSprintChecklist) {
207
- sprintChecklist = userSprintChecklist;
208
- sprintSource = 'user';
209
- userSprintLoaded = true;
210
- }
422
+ sprintFilePath = userSprintFile;
423
+ userSprintLoaded = true;
211
424
  }
212
425
  }
213
426
  catch {
@@ -215,72 +428,74 @@ export class StartReflectionCommand extends ReflectionCommand {
215
428
  // We'll fall back to global sprint
216
429
  }
217
430
  }
218
- // If no user sprint, load global sprint (CURRENT-SPRINT.md)
219
- let localSprint = null;
220
- if (!userSprintLoaded) {
221
- localSprint = await loadSprintChecklist(projectRoot);
222
- }
223
- // Only check graph/local if user sprint wasn't already loaded
224
- if (!userSprintLoaded) {
225
- // Try to load from graph if authenticated
226
- let graphSprint = null;
227
- if (await isAuthenticated() && await isGraphInitialized()) {
228
- try {
229
- spinner.text = 'Checking graph for sprint updates...';
230
- const graphId = await getGraphId();
231
- if (graphId) {
232
- const client = new GraphApiClient();
233
- const graphResponse = await client.getActiveSprint(graphId);
234
- graphSprint = this.convertGraphSprintToChecklist(graphResponse);
235
- }
236
- }
237
- catch (error) {
238
- // Graph unavailable - use local only
239
- spinner.text = 'Graph unavailable, using local sprint file';
240
- }
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);
241
464
  }
242
- // Compare and choose the more complete/current sprint
243
- // Priority: Local file is source of truth - it represents what the user is actively working on
244
- // Graph data may be stale (e.g., still showing Sprint 1 when user moved to Sprint 2)
245
- if (localSprint && graphSprint) {
246
- // If the sprints are different (different names), always use local
247
- // This handles the case where graph hasn't been synced with new sprint
248
- const localName = localSprint.name?.toLowerCase() || '';
249
- const graphName = graphSprint.name?.toLowerCase() || '';
250
- const sprintsAreDifferent = localName !== graphName &&
251
- !localName.includes(graphName) &&
252
- !graphName.includes(localName);
253
- if (sprintsAreDifferent) {
254
- // Local file is the source of truth for which sprint we're on
255
- sprintChecklist = localSprint;
256
- 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
+ }
257
478
  }
258
479
  else {
259
- // Same sprint - prefer local if it has more tasks OR higher completion count
260
- // This handles case where graph has stale/test data
261
- const localTotal = localSprint.progress?.total || 0;
262
- const graphTotal = graphSprint.progress?.total || 0;
263
- const localProgress = localSprint.progress?.complete || 0;
264
- const graphProgress = graphSprint.progress?.complete || 0;
265
- if (localTotal > graphTotal || localProgress > graphProgress) {
266
- 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;
267
485
  sprintSource = 'local';
268
- }
269
- else {
270
- sprintChecklist = graphSprint;
271
- sprintSource = 'graph';
486
+ isOffline = true;
272
487
  }
273
488
  }
274
489
  }
275
- else if (localSprint) {
276
- sprintChecklist = localSprint;
277
- sprintSource = 'local';
278
- }
279
- else if (graphSprint) {
280
- sprintChecklist = graphSprint;
281
- 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';
282
497
  }
283
- } // end if (!userSprintLoaded)
498
+ }
284
499
  // Load session log content before archiving
285
500
  const previousSessionLog = await SessionLogManager.loadSessionLog(sessionDir);
286
501
  const hasLog = previousSessionLog.length > 100; // Non-empty log
@@ -290,9 +505,9 @@ export class StartReflectionCommand extends ReflectionCommand {
290
505
  const synthesis = await synthesizer.synthesize();
291
506
  // 6. Archive previous session log (ALWAYS, not conditionally)
292
507
  if (hasLog) {
293
- spinner.text = 'Archiving previous session log...';
294
- const archivePath = await SessionLogManager.archiveLog(sessionDir);
295
- spinner.info(`Previous session archived: ${path.basename(archivePath)}`);
508
+ spinner.text = 'Archiving previous session...';
509
+ await SessionLogManager.archiveLog(sessionDir);
510
+ // Silent - table will be the only output
296
511
  }
297
512
  // 7. Determine work mode from context and update context manager
298
513
  const workMode = this.determineWorkMode(context, options);
@@ -317,14 +532,14 @@ export class StartReflectionCommand extends ReflectionCommand {
317
532
  teamEventLimit: 10,
318
533
  documentDepth: 2,
319
534
  teamDays: 7,
535
+ silent: true, // Suppress loading messages - table will be the only output
320
536
  }));
321
537
  // Ensure eventContext is defined
322
538
  if (!eventContext) {
323
539
  throw new Error('Event context loading failed');
324
540
  }
325
- // Display event-based context summary
326
- spinner.succeed('Event stream context loaded');
327
- console.log(chalk.dim(formatEventContextSummary(eventContext)));
541
+ // Silent - context summary will be shown in table
542
+ spinner.text = 'Processing events...';
328
543
  // Generate synthesis from loaded events (replaces file-based synthesis)
329
544
  eventSynthesis = await SessionSynthesizer.synthesizeFromEvents(eventContext, projectRoot);
330
545
  // Convert to strategy context for compatibility
@@ -397,12 +612,11 @@ export class StartReflectionCommand extends ReflectionCommand {
397
612
  spinner.text = 'Creating fresh session log...';
398
613
  const flowState = activeSynthesis?.flowState?.energy?.toLowerCase();
399
614
  await this.initializeSessionLog(context, options, flowState);
400
- if (!options.noLog) {
401
- spinner.info('Session logging enabled (use --no-log to disable)');
402
- }
615
+ // Session logging status shown in table if needed
403
616
  // 11. Build AI context for dual output system (TASK-11)
404
617
  // Now async due to graph API enrichment (EPIC-002 Sprint 3 completion)
405
- 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 });
406
620
  // Store AI context for MCP/external access
407
621
  await this.storeAIContext(aiContext, sessionDir);
408
622
  // 12. Check sprint progression and epic completion (EPIC-012 Sprint 1)
@@ -427,8 +641,8 @@ export class StartReflectionCommand extends ReflectionCommand {
427
641
  }
428
642
  }
429
643
  }
430
- // 13. Complete spinner before display
431
- spinner.succeed('Session initialized with strategic context!');
644
+ // 13. Stop spinner before any output
645
+ spinner.stop();
432
646
  // EPIC-008 Sprint 2: Check team context staleness (silent - no output)
433
647
  await this.checkTeamStaleness();
434
648
  // EPIC-004: Push real-time cursor update on session start
@@ -444,7 +658,7 @@ export class StartReflectionCommand extends ReflectionCommand {
444
658
  // Insights update is non-critical - don't block session start
445
659
  });
446
660
  // 14. Display output LAST (after all async operations complete)
447
- // Table view should be the final thing the user sees
661
+ // Table view is the FINAL output - nothing should print after it
448
662
  if (options.verbose) {
449
663
  // Verbose mode: Full session info (~80 lines)
450
664
  await this.displaySessionInfo(context, contextLevel, activeSynthesis, strategyContext, eventContext, sprintChecklist);
@@ -453,10 +667,11 @@ export class StartReflectionCommand extends ReflectionCommand {
453
667
  console.log(chalk.dim(formatContextSummary(strategyContext)));
454
668
  }
455
669
  else {
456
- // Default mode: Table view (use --compact for previous format)
670
+ // Default mode: Compact table (use --full for task list)
457
671
  this.displayConciseOutput(aiContext, {
458
672
  compact: options.compact,
459
- table: options.table
673
+ table: options.table,
674
+ full: options.full
460
675
  });
461
676
  }
462
677
  }
@@ -1255,7 +1470,7 @@ Example output structure:
1255
1470
  * Creates a rich, structured context object that AI partners can parse.
1256
1471
  * This is the "AI UX" side of the dual output system.
1257
1472
  */
1258
- async buildAIContext(context, synthesis, strategyContext, eventContext, sprintChecklist, isFirstTimeMember = false) {
1473
+ async buildAIContext(context, synthesis, strategyContext, eventContext, sprintChecklist, isFirstTimeMember = false, offlineStatus) {
1259
1474
  const workMode = context.workMode || 'Think & Build';
1260
1475
  // Build sprint object
1261
1476
  let sprint;
@@ -1338,6 +1553,10 @@ Example output structure:
1338
1553
  flowState: synthesis?.flowState?.energy || 'neutral',
1339
1554
  workMode: workMode,
1340
1555
  isFirstTimeMember,
1556
+ // EPIC-015 Sprint 2: Offline status indicators
1557
+ isOffline: offlineStatus?.isOffline,
1558
+ cacheAge: offlineStatus?.cacheAge,
1559
+ cacheStaleness: offlineStatus?.cacheStaleness,
1341
1560
  },
1342
1561
  charter: eventContext?.strategicContext?.charter,
1343
1562
  teamActivity: eventContext?.strategicContext?.teamActivity ? {
@@ -1387,13 +1606,14 @@ Example output structure:
1387
1606
  */
1388
1607
  displayConciseOutput(aiContext, options = {}) {
1389
1608
  console.log('');
1390
- // Table view is now the default
1391
- // --compact uses previous concise format
1392
- // --no-table (table === false) uses concise format
1609
+ // Default: Full table with task list
1610
+ // --compact: Previous concise format without borders
1611
+ // --no-table (table === false): Plain text format for piping
1393
1612
  if (options.compact || options.table === false) {
1394
1613
  console.log(formatHumanOutput(aiContext, { workMode: aiContext.session.workMode }));
1395
1614
  }
1396
1615
  else {
1616
+ // Default: full table with task list
1397
1617
  console.log(formatTableOutput(aiContext));
1398
1618
  }
1399
1619
  console.log('');