@ginkoai/cli 2.0.6 → 2.1.1

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 (149) 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 +221 -1
  10. package/dist/commands/graph/api-client.d.ts.map +1 -1
  11. package/dist/commands/graph/api-client.js +141 -2
  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/handoff.d.ts +3 -3
  17. package/dist/commands/handoff.d.ts.map +1 -1
  18. package/dist/commands/handoff.js +32 -3
  19. package/dist/commands/handoff.js.map +1 -1
  20. package/dist/commands/migrate/index.d.ts +27 -0
  21. package/dist/commands/migrate/index.d.ts.map +1 -0
  22. package/dist/commands/migrate/index.js +76 -0
  23. package/dist/commands/migrate/index.js.map +1 -0
  24. package/dist/commands/migrate/status-migration.d.ts +58 -0
  25. package/dist/commands/migrate/status-migration.d.ts.map +1 -0
  26. package/dist/commands/migrate/status-migration.js +323 -0
  27. package/dist/commands/migrate/status-migration.js.map +1 -0
  28. package/dist/commands/nudging/index.d.ts +24 -0
  29. package/dist/commands/nudging/index.d.ts.map +1 -0
  30. package/dist/commands/nudging/index.js +175 -0
  31. package/dist/commands/nudging/index.js.map +1 -0
  32. package/dist/commands/sprint/create.d.ts +26 -0
  33. package/dist/commands/sprint/create.d.ts.map +1 -0
  34. package/dist/commands/sprint/create.js +269 -0
  35. package/dist/commands/sprint/create.js.map +1 -0
  36. package/dist/commands/sprint/index.d.ts.map +1 -1
  37. package/dist/commands/sprint/index.js +28 -0
  38. package/dist/commands/sprint/index.js.map +1 -1
  39. package/dist/commands/sprint/quick-fix.d.ts +25 -0
  40. package/dist/commands/sprint/quick-fix.d.ts.map +1 -0
  41. package/dist/commands/sprint/quick-fix.js +151 -0
  42. package/dist/commands/sprint/quick-fix.js.map +1 -0
  43. package/dist/commands/sprint/sprint-pipeline-enhanced.d.ts.map +1 -1
  44. package/dist/commands/sprint/sprint-pipeline-enhanced.js +37 -0
  45. package/dist/commands/sprint/sprint-pipeline-enhanced.js.map +1 -1
  46. package/dist/commands/sprint/status.d.ts +42 -0
  47. package/dist/commands/sprint/status.d.ts.map +1 -0
  48. package/dist/commands/sprint/status.js +298 -0
  49. package/dist/commands/sprint/status.js.map +1 -0
  50. package/dist/commands/start/start-reflection.d.ts +53 -0
  51. package/dist/commands/start/start-reflection.d.ts.map +1 -1
  52. package/dist/commands/start/start-reflection.js +464 -71
  53. package/dist/commands/start/start-reflection.js.map +1 -1
  54. package/dist/commands/sync/sprint-syncer.d.ts +19 -12
  55. package/dist/commands/sync/sprint-syncer.d.ts.map +1 -1
  56. package/dist/commands/sync/sprint-syncer.js +58 -140
  57. package/dist/commands/sync/sprint-syncer.js.map +1 -1
  58. package/dist/commands/sync/sync-command.d.ts.map +1 -1
  59. package/dist/commands/sync/sync-command.js +6 -18
  60. package/dist/commands/sync/sync-command.js.map +1 -1
  61. package/dist/commands/task/index.d.ts +25 -0
  62. package/dist/commands/task/index.d.ts.map +1 -0
  63. package/dist/commands/task/index.js +100 -0
  64. package/dist/commands/task/index.js.map +1 -0
  65. package/dist/commands/task/status.d.ts +46 -0
  66. package/dist/commands/task/status.d.ts.map +1 -0
  67. package/dist/commands/task/status.js +348 -0
  68. package/dist/commands/task/status.js.map +1 -0
  69. package/dist/commands/team/index.d.ts +5 -0
  70. package/dist/commands/team/index.d.ts.map +1 -1
  71. package/dist/commands/team/index.js +28 -0
  72. package/dist/commands/team/index.js.map +1 -1
  73. package/dist/commands/team/status.d.ts +16 -0
  74. package/dist/commands/team/status.d.ts.map +1 -0
  75. package/dist/commands/team/status.js +180 -0
  76. package/dist/commands/team/status.js.map +1 -0
  77. package/dist/index.js +21 -32
  78. package/dist/index.js.map +1 -1
  79. package/dist/lib/adoption-score.d.ts +69 -0
  80. package/dist/lib/adoption-score.d.ts.map +1 -0
  81. package/dist/lib/adoption-score.js +206 -0
  82. package/dist/lib/adoption-score.js.map +1 -0
  83. package/dist/lib/coaching-level.d.ts +127 -0
  84. package/dist/lib/coaching-level.d.ts.map +1 -0
  85. package/dist/lib/coaching-level.js +406 -0
  86. package/dist/lib/coaching-level.js.map +1 -0
  87. package/dist/lib/context-loader-events.d.ts.map +1 -1
  88. package/dist/lib/context-loader-events.js +7 -26
  89. package/dist/lib/context-loader-events.js.map +1 -1
  90. package/dist/lib/event-logger.d.ts +42 -0
  91. package/dist/lib/event-logger.d.ts.map +1 -1
  92. package/dist/lib/event-logger.js +77 -0
  93. package/dist/lib/event-logger.js.map +1 -1
  94. package/dist/lib/output-formatter.d.ts +8 -2
  95. package/dist/lib/output-formatter.d.ts.map +1 -1
  96. package/dist/lib/output-formatter.js +98 -18
  97. package/dist/lib/output-formatter.js.map +1 -1
  98. package/dist/lib/pending-updates.d.ts +148 -0
  99. package/dist/lib/pending-updates.d.ts.map +1 -0
  100. package/dist/lib/pending-updates.js +301 -0
  101. package/dist/lib/pending-updates.js.map +1 -0
  102. package/dist/lib/planning-menu.d.ts +69 -0
  103. package/dist/lib/planning-menu.d.ts.map +1 -0
  104. package/dist/lib/planning-menu.js +342 -0
  105. package/dist/lib/planning-menu.js.map +1 -0
  106. package/dist/lib/sprint-loader.d.ts +86 -14
  107. package/dist/lib/sprint-loader.d.ts.map +1 -1
  108. package/dist/lib/sprint-loader.js +293 -98
  109. package/dist/lib/sprint-loader.js.map +1 -1
  110. package/dist/lib/state-cache.d.ts +142 -0
  111. package/dist/lib/state-cache.d.ts.map +1 -0
  112. package/dist/lib/state-cache.js +259 -0
  113. package/dist/lib/state-cache.js.map +1 -0
  114. package/dist/lib/targeted-coaching.d.ts +71 -0
  115. package/dist/lib/targeted-coaching.d.ts.map +1 -0
  116. package/dist/lib/targeted-coaching.js +318 -0
  117. package/dist/lib/targeted-coaching.js.map +1 -0
  118. package/dist/lib/task-graph-sync.d.ts +105 -0
  119. package/dist/lib/task-graph-sync.d.ts.map +1 -0
  120. package/dist/lib/task-graph-sync.js +178 -0
  121. package/dist/lib/task-graph-sync.js.map +1 -0
  122. package/dist/lib/task-parser.d.ts +109 -0
  123. package/dist/lib/task-parser.d.ts.map +1 -0
  124. package/dist/lib/task-parser.js +407 -0
  125. package/dist/lib/task-parser.js.map +1 -0
  126. package/dist/lib/user-sprint.d.ts +53 -0
  127. package/dist/lib/user-sprint.d.ts.map +1 -1
  128. package/dist/lib/user-sprint.js +137 -2
  129. package/dist/lib/user-sprint.js.map +1 -1
  130. package/dist/lib/work-reconciliation.d.ts +59 -0
  131. package/dist/lib/work-reconciliation.d.ts.map +1 -0
  132. package/dist/lib/work-reconciliation.js +267 -0
  133. package/dist/lib/work-reconciliation.js.map +1 -0
  134. package/dist/templates/ai-instructions-template.d.ts.map +1 -1
  135. package/dist/templates/ai-instructions-template.js +7 -5
  136. package/dist/templates/ai-instructions-template.js.map +1 -1
  137. package/dist/templates/epic-template.md +0 -2
  138. package/dist/types/config.d.ts.map +1 -1
  139. package/dist/types/config.js +7 -5
  140. package/dist/types/config.js.map +1 -1
  141. package/dist/utils/synthesis.d.ts +4 -0
  142. package/dist/utils/synthesis.d.ts.map +1 -1
  143. package/dist/utils/synthesis.js +12 -18
  144. package/dist/utils/synthesis.js.map +1 -1
  145. package/dist/utils/version-check.d.ts +26 -0
  146. package/dist/utils/version-check.d.ts.map +1 -0
  147. package/dist/utils/version-check.js +186 -0
  148. package/dist/utils/version-check.js.map +1 -0
  149. package/package.json +2 -2
@@ -14,15 +14,25 @@ import fs from 'fs-extra';
14
14
  import * as path from 'path';
15
15
  import chalk from 'chalk';
16
16
  import ora from 'ora';
17
+ import readline from 'readline';
17
18
  import { getUserEmail, getGinkoDir, getProjectRoot } from '../../utils/helpers.js';
19
+ import { checkForUpdatesAsync } from '../../utils/version-check.js';
18
20
  import { ActiveContextManager } from '../../services/active-context-manager.js';
19
21
  import { SessionLogManager } from '../../core/session-log-manager.js';
20
22
  import { SessionSynthesizer } from '../../utils/synthesis.js';
21
23
  import { loadContextStrategic, formatContextSummary } from '../../utils/context-loader.js';
22
24
  import { initializeQueue } from '../../lib/event-queue.js';
23
25
  import { loadSprintChecklist, formatSprintChecklist, formatCurrentTaskDetails, detectSprintProgression } from '../../lib/sprint-loader.js';
26
+ import { loadStateCache, saveStateCache, checkCacheStaleness } from '../../lib/state-cache.js';
24
27
  import { formatHumanOutput, formatVerboseOutput, formatAIContextJSONL, formatTableOutput, formatEpicComplete, formatSprintProgressionPrompt } from '../../lib/output-formatter.js';
25
- import { getUserCurrentSprint, setUserCurrentSprint, createAssignmentFromFile, getSprintFileFromAssignment } from '../../lib/user-sprint.js';
28
+ import { getUserCurrentSprint, setUserCurrentSprint, createAssignmentFromFile, getSprintFileFromAssignment,
29
+ // EPIC-016 Sprint 4: Structure detection (t01)
30
+ checkWorkStructure, incrementAdhocSessionCount, resetAdhocSessionCount } from '../../lib/user-sprint.js';
31
+ // EPIC-016 Sprint 4: Planning menu (t02)
32
+ import { showPlanningMenu, routePlanningChoice } from '../../lib/planning-menu.js';
33
+ // EPIC-016 Sprint 5: Adaptive coaching (t01, t03)
34
+ import { getCoachingContext } from '../../lib/coaching-level.js';
35
+ import { showTargetedCoaching } from '../../lib/targeted-coaching.js';
26
36
  import { GraphApiClient } from '../graph/api-client.js';
27
37
  import { isGraphInitialized, getGraphId } from '../graph/config.js';
28
38
  import { isAuthenticated } from '../../utils/auth-storage.js';
@@ -156,12 +166,231 @@ export class StartReflectionCommand extends ReflectionCommand {
156
166
  source: 'graph', // Mark source for debugging
157
167
  };
158
168
  }
169
+ /**
170
+ * Load sprint state from graph API with cache fallback (EPIC-015 Sprint 2 Task 3)
171
+ *
172
+ * Data flow:
173
+ * 1. Try to fetch from graph API first
174
+ * 2. On success: save to cache for offline use
175
+ * 3. On failure: load from cache
176
+ *
177
+ * @param graphId - The graph namespace ID
178
+ * @returns Sprint data and source indicator ('graph' | 'cache' | null)
179
+ */
180
+ async loadSprintStateFromGraph(graphId) {
181
+ const client = new GraphApiClient();
182
+ try {
183
+ // Try to fetch from graph API first
184
+ const graphResponse = await client.getActiveSprint(graphId);
185
+ // Convert API response to cache format
186
+ const activeSprintData = {
187
+ sprintId: graphResponse.sprint.id,
188
+ sprintName: graphResponse.sprint.name,
189
+ epicId: graphResponse.sprint.id.split('_s')[0] || 'unknown', // Extract epic from e011_s01
190
+ progress: {
191
+ completed: graphResponse.stats.completedTasks,
192
+ total: graphResponse.stats.totalTasks,
193
+ percentage: graphResponse.stats.progressPercentage,
194
+ },
195
+ currentTask: graphResponse.nextTask ? {
196
+ taskId: graphResponse.nextTask.id,
197
+ taskName: graphResponse.nextTask.title,
198
+ status: this.mapGraphStatusToTaskStatus(graphResponse.nextTask.status),
199
+ } : undefined,
200
+ nextTask: graphResponse.nextTask ? {
201
+ taskId: graphResponse.nextTask.id,
202
+ taskName: graphResponse.nextTask.title,
203
+ } : undefined,
204
+ };
205
+ // Save to cache for offline use
206
+ await saveStateCache(activeSprintData, graphId);
207
+ return {
208
+ data: activeSprintData,
209
+ source: 'graph',
210
+ };
211
+ }
212
+ catch (error) {
213
+ // Graph unavailable - try to load from cache
214
+ const cache = await loadStateCache();
215
+ if (cache && cache.graph_id === graphId) {
216
+ const staleness = checkCacheStaleness(cache);
217
+ return {
218
+ data: cache.active_sprint,
219
+ source: 'cache',
220
+ staleness,
221
+ };
222
+ }
223
+ // No cache available
224
+ return {
225
+ data: null,
226
+ source: 'cache',
227
+ };
228
+ }
229
+ }
230
+ /**
231
+ * Load sprint data from cache only (for offline fallback)
232
+ */
233
+ async loadFromCacheOnly(graphId) {
234
+ const cache = await loadStateCache();
235
+ if (cache && cache.graph_id === graphId) {
236
+ const staleness = checkCacheStaleness(cache);
237
+ return {
238
+ data: cache.active_sprint,
239
+ staleness,
240
+ };
241
+ }
242
+ return { data: null };
243
+ }
244
+ /**
245
+ * Convert cached ActiveSprintData to SprintChecklist format
246
+ */
247
+ convertCachedDataToChecklist(cachedData) {
248
+ // Build minimal checklist from cached data
249
+ const tasks = [];
250
+ // If we have a current task, add it
251
+ if (cachedData.currentTask) {
252
+ tasks.push({
253
+ id: cachedData.currentTask.taskId,
254
+ title: cachedData.currentTask.taskName,
255
+ state: this.mapStatusToTaskState(cachedData.currentTask.status || 'todo'),
256
+ });
257
+ }
258
+ const completed = cachedData.progress?.completed || 0;
259
+ const total = cachedData.progress?.total || tasks.length;
260
+ return {
261
+ name: cachedData.sprintName || cachedData.sprintId,
262
+ file: '', // No file when loading from cache
263
+ tasks,
264
+ progress: {
265
+ complete: completed,
266
+ inProgress: cachedData.currentTask ? 1 : 0,
267
+ paused: 0,
268
+ todo: Math.max(0, total - completed - (cachedData.currentTask ? 1 : 0)),
269
+ total,
270
+ },
271
+ currentTask: cachedData.currentTask ? {
272
+ id: cachedData.currentTask.taskId,
273
+ title: cachedData.currentTask.taskName,
274
+ state: this.mapStatusToTaskState(cachedData.currentTask.status || 'todo'),
275
+ } : undefined,
276
+ recentCompletions: [],
277
+ };
278
+ }
279
+ /**
280
+ * Map graph API status string to TaskStatus enum
281
+ */
282
+ mapGraphStatusToTaskStatus(status) {
283
+ switch (status?.toLowerCase()) {
284
+ case 'complete':
285
+ case 'completed':
286
+ return 'completed';
287
+ case 'in_progress':
288
+ case 'in-progress':
289
+ return 'in_progress';
290
+ default:
291
+ return 'pending';
292
+ }
293
+ }
294
+ /**
295
+ * Map graph status to TaskState enum for SprintChecklist
296
+ */
297
+ mapStatusToTaskState(status) {
298
+ switch (status?.toLowerCase()) {
299
+ case 'complete':
300
+ case 'completed':
301
+ return 'complete';
302
+ case 'in_progress':
303
+ case 'in-progress':
304
+ return 'in_progress';
305
+ case 'paused':
306
+ case 'sleeping':
307
+ return 'paused';
308
+ case 'blocked':
309
+ return 'in_progress'; // Treat blocked as in_progress for display
310
+ default:
311
+ return 'todo';
312
+ }
313
+ }
314
+ /**
315
+ * Merge graph status with local file content (EPIC-015 Sprint 2 Task 3)
316
+ *
317
+ * Graph API is authoritative for STATUS (progress, task states).
318
+ * Local file provides CONTENT (descriptions, acceptance criteria, ADRs).
319
+ *
320
+ * @param graphData - Status data from graph API (or cache)
321
+ * @param fileContent - Content from local sprint file
322
+ * @returns Merged SprintChecklist with graph status + file content
323
+ */
324
+ mergeGraphStatusWithContent(graphData, fileContent, graphTasks) {
325
+ // Build task list: merge graph status with file content
326
+ const tasks = fileContent.tasks.map((fileTask) => {
327
+ // Find matching task in graph data
328
+ const graphTask = graphTasks?.find(gt => gt.id === fileTask.id);
329
+ const status = graphTask?.status || 'not_started';
330
+ return {
331
+ id: fileTask.id,
332
+ title: fileTask.title,
333
+ state: this.mapStatusToTaskState(status),
334
+ files: fileTask.files || graphTask?.files || [],
335
+ effort: fileTask.effort,
336
+ priority: fileTask.priority || graphTask?.priority,
337
+ relatedADRs: fileTask.relatedADRs,
338
+ relatedPatterns: fileTask.relatedPatterns,
339
+ relatedGotchas: fileTask.relatedGotchas,
340
+ acceptanceCriteria: fileTask.acceptanceCriteria,
341
+ dependsOn: fileTask.dependsOn,
342
+ };
343
+ });
344
+ // Calculate progress from graph data
345
+ const progress = {
346
+ complete: graphData.progress.completed,
347
+ inProgress: tasks.filter(t => t.state === 'in_progress').length,
348
+ paused: tasks.filter(t => t.state === 'paused').length,
349
+ todo: graphData.progress.total - graphData.progress.completed - tasks.filter(t => t.state === 'in_progress').length,
350
+ total: graphData.progress.total,
351
+ };
352
+ // Determine current task (first in_progress or first todo)
353
+ const currentTask = tasks.find(t => t.state === 'in_progress') ||
354
+ tasks.find(t => t.state === 'todo');
355
+ // Recent completions (completed tasks from the end of list)
356
+ const recentCompletions = tasks
357
+ .filter(t => t.state === 'complete')
358
+ .slice(-3);
359
+ return {
360
+ name: graphData.sprintName,
361
+ file: fileContent.file,
362
+ progress,
363
+ tasks,
364
+ currentTask,
365
+ recentCompletions,
366
+ dependencyWarnings: fileContent.dependencyWarnings,
367
+ };
368
+ }
159
369
  /**
160
370
  * Override execute to process handoff and display session info
161
371
  */
162
372
  async execute(intent, options = {}) {
163
- const spinner = ora('Initializing session...').start();
373
+ // TTY detection with override for Claude Code (e014_s02_t03)
374
+ // Claude Code's terminal supports box-drawing and colors but reports isTTY=false
375
+ // Use GINKO_FORCE_TTY=1 to force TTY-like output in Claude Code
376
+ const forceTTY = process.env.GINKO_FORCE_TTY === '1' || process.env.GINKO_FORCE_TTY === 'true';
377
+ const isTTY = forceTTY || process.stdout.isTTY === true;
378
+ // Also force chalk colors when forcing TTY mode (chalk checks FORCE_COLOR env var)
379
+ if (forceTTY && !process.env.FORCE_COLOR) {
380
+ process.env.FORCE_COLOR = '1';
381
+ }
382
+ const spinner = ora({
383
+ text: 'Initializing session...',
384
+ isEnabled: isTTY,
385
+ isSilent: !isTTY, // Completely silence output in non-TTY mode
386
+ });
387
+ // Only start spinner animation in TTY mode
388
+ if (isTTY) {
389
+ spinner.start();
390
+ }
164
391
  try {
392
+ // 0. Start version check in background (non-blocking)
393
+ const versionCheckPromise = checkForUpdatesAsync();
165
394
  // 1. Parse intent
166
395
  const parsedIntent = this.parseIntent(intent);
167
396
  // 2. Initialize event queue for background sync (ADR-043)
@@ -188,24 +417,27 @@ export class StartReflectionCommand extends ReflectionCommand {
188
417
  const userSlug = userEmail.replace('@', '-at-').replace(/\./g, '-');
189
418
  const sessionDir = path.join(ginkoDir, 'sessions', userSlug);
190
419
  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
420
+ // Load sprint checklist for session context (EPIC-015 Sprint 2 Task 3)
421
+ // NEW DATA FLOW: Graph API is authoritative for STATUS, file provides CONTENT
422
+ // 1. Fetch status from graph API (with cache fallback)
423
+ // 2. Load content from local sprint file
424
+ // 3. Merge: graph status + file content
194
425
  let sprintChecklist = null;
195
426
  let sprintSource = 'local';
427
+ let isOffline = false;
428
+ let cacheAge;
429
+ let cacheStaleness;
196
430
  // First, check if user has a specific sprint assignment
197
- const userSprint = await getUserCurrentSprint();
431
+ // EPIC-012: Per-user sprint tracking enables multiple users on different sprints
432
+ let userSprint = await getUserCurrentSprint();
198
433
  let userSprintLoaded = false;
434
+ let sprintFilePath;
199
435
  if (userSprint) {
200
436
  try {
201
437
  const userSprintFile = await getSprintFileFromAssignment(userSprint);
202
438
  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
- }
439
+ sprintFilePath = userSprintFile;
440
+ userSprintLoaded = true;
209
441
  }
210
442
  }
211
443
  catch {
@@ -213,72 +445,86 @@ export class StartReflectionCommand extends ReflectionCommand {
213
445
  // We'll fall back to global sprint
214
446
  }
215
447
  }
216
- // If no user sprint, load global sprint (CURRENT-SPRINT.md)
217
- let localSprint = null;
218
- if (!userSprintLoaded) {
219
- localSprint = await loadSprintChecklist(projectRoot);
448
+ // Load sprint with graph-first status (EPIC-015 Sprint 2 Task 3)
449
+ const graphId = await isGraphInitialized() ? await getGraphId() : null;
450
+ const isGraphReady = await isAuthenticated() && graphId;
451
+ // When user has explicitly set a sprint (via `ginko sprint start`), respect that choice
452
+ // This ensures continuity - the user knows what they want to work on
453
+ if (userSprintLoaded && sprintFilePath && sprintFilePath.length > 0) {
454
+ // User has a local sprint file - load from file
455
+ spinner.text = 'Loading user-selected sprint...';
456
+ const localChecklist = await loadSprintChecklist(projectRoot, sprintFilePath);
457
+ if (localChecklist) {
458
+ sprintChecklist = localChecklist;
459
+ sprintSource = 'user';
460
+ }
220
461
  }
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
- }
462
+ else if (isGraphReady && graphId) {
463
+ // Fetch from graph - pass user's sprint preference if set (API will prioritize it)
464
+ spinner.text = 'Fetching sprint from graph...';
465
+ const client = new GraphApiClient();
466
+ try {
467
+ // Pass user's sprint preference if set (API will prioritize it for continuity)
468
+ const preferredSprintId = userSprint?.sprintId;
469
+ const graphResponse = await client.getActiveSprint(graphId, preferredSprintId);
470
+ sprintChecklist = this.convertGraphSprintToChecklist(graphResponse);
471
+ sprintSource = 'graph';
472
+ // Save to cache for offline fallback
473
+ const activeSprintData = {
474
+ sprintId: graphResponse.sprint.id,
475
+ sprintName: graphResponse.sprint.name,
476
+ epicId: graphResponse.sprint.id.split('_s')[0] || 'unknown',
477
+ progress: {
478
+ completed: graphResponse.stats.completedTasks,
479
+ total: graphResponse.stats.totalTasks,
480
+ percentage: graphResponse.stats.progressPercentage,
481
+ },
482
+ currentTask: graphResponse.nextTask ? {
483
+ taskId: graphResponse.nextTask.id,
484
+ taskName: graphResponse.nextTask.title,
485
+ status: this.mapGraphStatusToTaskStatus(graphResponse.nextTask.status),
486
+ } : undefined,
487
+ nextTask: graphResponse.nextTask ? {
488
+ taskId: graphResponse.nextTask.id,
489
+ taskName: graphResponse.nextTask.title,
490
+ } : undefined,
491
+ };
492
+ await saveStateCache(activeSprintData, graphId);
239
493
  }
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';
494
+ catch (error) {
495
+ // Graph unavailable - try cache fallback
496
+ const { data: cachedData, staleness } = await this.loadFromCacheOnly(graphId);
497
+ if (cachedData) {
498
+ // Use cached data
499
+ sprintChecklist = this.convertCachedDataToChecklist(cachedData);
500
+ sprintSource = 'cache';
501
+ isOffline = true;
502
+ cacheAge = staleness?.ageHuman;
503
+ cacheStaleness = staleness?.level;
504
+ if (staleness?.showWarning) {
505
+ spinner.text = `Using cached sprint (${staleness.ageHuman})`;
506
+ }
255
507
  }
256
508
  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;
509
+ // No cache - fall back to local file (offline fallback only)
510
+ spinner.text = 'Graph unavailable, using local sprint file';
511
+ const localChecklist = await loadSprintChecklist(projectRoot, sprintFilePath);
512
+ if (localChecklist) {
513
+ sprintChecklist = localChecklist;
265
514
  sprintSource = 'local';
266
- }
267
- else {
268
- sprintChecklist = graphSprint;
269
- sprintSource = 'graph';
515
+ isOffline = true;
270
516
  }
271
517
  }
272
518
  }
273
- else if (localSprint) {
274
- sprintChecklist = localSprint;
275
- sprintSource = 'local';
276
- }
277
- else if (graphSprint) {
278
- sprintChecklist = graphSprint;
279
- sprintSource = 'graph';
519
+ }
520
+ else {
521
+ // Graph not initialized - use local file only
522
+ const localChecklist = await loadSprintChecklist(projectRoot, sprintFilePath);
523
+ if (localChecklist) {
524
+ sprintChecklist = localChecklist;
525
+ sprintSource = userSprintLoaded ? 'user' : 'local';
280
526
  }
281
- } // end if (!userSprintLoaded)
527
+ }
282
528
  // Load session log content before archiving
283
529
  const previousSessionLog = await SessionLogManager.loadSessionLog(sessionDir);
284
530
  const hasLog = previousSessionLog.length > 100; // Non-empty log
@@ -398,7 +644,8 @@ export class StartReflectionCommand extends ReflectionCommand {
398
644
  // Session logging status shown in table if needed
399
645
  // 11. Build AI context for dual output system (TASK-11)
400
646
  // Now async due to graph API enrichment (EPIC-002 Sprint 3 completion)
401
- const aiContext = await this.buildAIContext(context, activeSynthesis, strategyContext, eventContext, sprintChecklist, isFirstTimeMember);
647
+ // EPIC-015 Sprint 2: Include offline status for display
648
+ const aiContext = await this.buildAIContext(context, activeSynthesis, strategyContext, eventContext, sprintChecklist, isFirstTimeMember, { isOffline, cacheAge, cacheStaleness });
402
649
  // Store AI context for MCP/external access
403
650
  await this.storeAIContext(aiContext, sessionDir);
404
651
  // 12. Check sprint progression and epic completion (EPIC-012 Sprint 1)
@@ -425,6 +672,42 @@ export class StartReflectionCommand extends ReflectionCommand {
425
672
  }
426
673
  // 13. Stop spinner before any output
427
674
  spinner.stop();
675
+ // EPIC-016 Sprint 4: Work Pattern Coaching - Structure Detection (t01) and Planning Menu (t02)
676
+ // Check if user has structured work (Epic→Sprint→Task). If not, show guided planning menu.
677
+ const structureStatus = checkWorkStructure(userSprint, sprintChecklist);
678
+ if (structureStatus.shouldShowPlanningMenu) {
679
+ // Show planning menu to guide user toward structured work
680
+ const menuResult = await showPlanningMenu(structureStatus);
681
+ if (menuResult.cancelled) {
682
+ console.log(chalk.dim('Session start cancelled.'));
683
+ return;
684
+ }
685
+ // Route the selection
686
+ const routeResult = await routePlanningChoice(menuResult.choice);
687
+ // Track adoption: ad-hoc vs structured choice
688
+ if (menuResult.choice === 'adhoc') {
689
+ await incrementAdhocSessionCount();
690
+ }
691
+ else if (routeResult.success) {
692
+ // User chose structured work - reset ad-hoc counter
693
+ await resetAdhocSessionCount();
694
+ }
695
+ // Epic and sprint creation handle their own flow
696
+ if (!routeResult.shouldContinueStart) {
697
+ return;
698
+ }
699
+ // For quick-fix and ad-hoc, update sprint info if a sprint was created
700
+ if (routeResult.sprintId) {
701
+ userSprint = await getUserCurrentSprint();
702
+ // Could refresh sprintChecklist from graph here if needed
703
+ }
704
+ console.log(''); // Spacing before normal output continues
705
+ }
706
+ // EPIC-016 Sprint 4: Check for unassigned tasks and prompt for bulk assignment (ADR-061)
707
+ // Per ADR-061: Work cannot be anonymous - prompt at sprint start for assignment
708
+ if (sprintChecklist && graphId) {
709
+ sprintChecklist = await this.checkAndPromptBulkAssignment(sprintChecklist, graphId, userEmail);
710
+ }
428
711
  // EPIC-008 Sprint 2: Check team context staleness (silent - no output)
429
712
  await this.checkTeamStaleness();
430
713
  // EPIC-004: Push real-time cursor update on session start
@@ -435,10 +718,32 @@ export class StartReflectionCommand extends ReflectionCommand {
435
718
  catch {
436
719
  // Cursor update is non-critical - don't block session start
437
720
  }
721
+ // EPIC-016 Sprint 3: Record session start activity
722
+ try {
723
+ const activityGraphId = await getGraphId();
724
+ if (activityGraphId) {
725
+ const activityClient = new GraphApiClient();
726
+ await activityClient.recordActivity(activityGraphId, 'session_start');
727
+ }
728
+ }
729
+ catch {
730
+ // Activity tracking is non-critical - don't block session start
731
+ }
438
732
  // Auto-update insights at scheduled intervals (1-day, 7-day, 30-day)
439
733
  this.runScheduledInsights().catch(() => {
440
734
  // Insights update is non-critical - don't block session start
441
735
  });
736
+ // EPIC-016 Sprint 5 t03: Show targeted coaching tips at session start
737
+ // Only show if we didn't just show the planning menu (which has its own coaching)
738
+ if (!structureStatus.shouldShowPlanningMenu) {
739
+ try {
740
+ const coachingContext = await getCoachingContext(graphId || undefined);
741
+ await showTargetedCoaching(coachingContext, 'start');
742
+ }
743
+ catch {
744
+ // Coaching tips are non-critical - don't block session start
745
+ }
746
+ }
442
747
  // 14. Display output LAST (after all async operations complete)
443
748
  // Table view is the FINAL output - nothing should print after it
444
749
  if (options.verbose) {
@@ -456,6 +761,11 @@ export class StartReflectionCommand extends ReflectionCommand {
456
761
  full: options.full
457
762
  });
458
763
  }
764
+ // 15. Show update notification if available (non-blocking check completed)
765
+ const updateMessage = await versionCheckPromise;
766
+ if (updateMessage) {
767
+ console.log(updateMessage);
768
+ }
459
769
  }
460
770
  catch (error) {
461
771
  spinner.fail('Session initialization failed');
@@ -1252,7 +1562,7 @@ Example output structure:
1252
1562
  * Creates a rich, structured context object that AI partners can parse.
1253
1563
  * This is the "AI UX" side of the dual output system.
1254
1564
  */
1255
- async buildAIContext(context, synthesis, strategyContext, eventContext, sprintChecklist, isFirstTimeMember = false) {
1565
+ async buildAIContext(context, synthesis, strategyContext, eventContext, sprintChecklist, isFirstTimeMember = false, offlineStatus) {
1256
1566
  const workMode = context.workMode || 'Think & Build';
1257
1567
  // Build sprint object
1258
1568
  let sprint;
@@ -1335,6 +1645,10 @@ Example output structure:
1335
1645
  flowState: synthesis?.flowState?.energy || 'neutral',
1336
1646
  workMode: workMode,
1337
1647
  isFirstTimeMember,
1648
+ // EPIC-015 Sprint 2: Offline status indicators
1649
+ isOffline: offlineStatus?.isOffline,
1650
+ cacheAge: offlineStatus?.cacheAge,
1651
+ cacheStaleness: offlineStatus?.cacheStaleness,
1338
1652
  },
1339
1653
  charter: eventContext?.strategicContext?.charter,
1340
1654
  teamActivity: eventContext?.strategicContext?.teamActivity ? {
@@ -1459,6 +1773,85 @@ Example output structure:
1459
1773
  // Staleness check is non-critical - don't block session start
1460
1774
  }
1461
1775
  }
1776
+ /**
1777
+ * Check for unassigned tasks in the active sprint and prompt for bulk assignment
1778
+ * Per ADR-061: Work cannot be anonymous - encourage assignment at sprint start
1779
+ *
1780
+ * @param sprintChecklist - Current sprint data with tasks
1781
+ * @param graphId - Graph ID for API calls
1782
+ * @param userEmail - Current user's email
1783
+ * @returns Updated sprintChecklist if tasks were assigned
1784
+ */
1785
+ async checkAndPromptBulkAssignment(sprintChecklist, graphId, userEmail) {
1786
+ // Skip if no sprint or no graph
1787
+ if (!sprintChecklist || !graphId) {
1788
+ return sprintChecklist;
1789
+ }
1790
+ // Count unassigned tasks from graph
1791
+ const client = new GraphApiClient();
1792
+ // Extract sprint ID from file or name, handling both formats:
1793
+ // - e016_s04 (underscore - API format)
1794
+ // - e016-s04 (hyphen - file naming format)
1795
+ const sourceString = sprintChecklist.file || sprintChecklist.name || '';
1796
+ const sprintIdMatch = sourceString.match(/e\d+[-_]s\d+/i);
1797
+ if (!sprintIdMatch) {
1798
+ return sprintChecklist;
1799
+ }
1800
+ // Normalize to underscore format for API calls
1801
+ const sprintId = sprintIdMatch[0].replace('-', '_').toLowerCase();
1802
+ try {
1803
+ // Fetch tasks from graph to check assignee status
1804
+ const tasks = await client.getTasks(graphId, { sprintId });
1805
+ const unassignedTasks = tasks.filter(t => !t.assignee || t.assignee === '' || t.assignee === null);
1806
+ if (unassignedTasks.length === 0) {
1807
+ return sprintChecklist;
1808
+ }
1809
+ // Prompt user for bulk assignment (ADR-061)
1810
+ console.log('');
1811
+ console.log(chalk.yellow(`📋 ${unassignedTasks.length} unassigned ${unassignedTasks.length === 1 ? 'task' : 'tasks'} in sprint ${sprintId}`));
1812
+ const shouldAssign = await this.promptConfirm(`Assign all to you (${userEmail})?`);
1813
+ if (shouldAssign) {
1814
+ // Bulk assign all tasks
1815
+ const assignSpinner = ora({
1816
+ text: `Assigning ${unassignedTasks.length} tasks...`,
1817
+ isEnabled: process.stdout.isTTY || process.env.GINKO_FORCE_TTY === '1',
1818
+ }).start();
1819
+ try {
1820
+ // Update each task's assignee
1821
+ for (const task of unassignedTasks) {
1822
+ await client.request('PATCH', `/api/v1/graph/nodes/${encodeURIComponent(task.id)}?graphId=${encodeURIComponent(graphId)}`, { assignee: userEmail });
1823
+ }
1824
+ assignSpinner.succeed(chalk.green(`✓ Assigned ${unassignedTasks.length} tasks to ${userEmail}`));
1825
+ }
1826
+ catch (error) {
1827
+ assignSpinner.fail(chalk.red(`Failed to assign tasks: ${error.message}`));
1828
+ }
1829
+ }
1830
+ else {
1831
+ console.log(chalk.dim(' Skipped - tasks remain unassigned'));
1832
+ }
1833
+ console.log('');
1834
+ }
1835
+ catch {
1836
+ // Non-blocking - if we can't check assignments, continue
1837
+ }
1838
+ return sprintChecklist;
1839
+ }
1840
+ /**
1841
+ * Prompt user for yes/no confirmation
1842
+ */
1843
+ async promptConfirm(message) {
1844
+ const rl = readline.createInterface({
1845
+ input: process.stdin,
1846
+ output: process.stdout,
1847
+ });
1848
+ return new Promise((resolve) => {
1849
+ rl.question(`${message} [Y/n] `, (answer) => {
1850
+ rl.close();
1851
+ resolve(answer.toLowerCase() !== 'n');
1852
+ });
1853
+ });
1854
+ }
1462
1855
  /**
1463
1856
  * Run scheduled insights updates in the background.
1464
1857
  * Checks which periods (1-day, 7-day, 30-day) need updates and runs them.