@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.
- package/dist/commands/epic/index.d.ts +29 -0
- package/dist/commands/epic/index.d.ts.map +1 -0
- package/dist/commands/epic/index.js +94 -0
- package/dist/commands/epic/index.js.map +1 -0
- package/dist/commands/epic/status.d.ts +40 -0
- package/dist/commands/epic/status.d.ts.map +1 -0
- package/dist/commands/epic/status.js +244 -0
- package/dist/commands/epic/status.js.map +1 -0
- package/dist/commands/graph/api-client.d.ts +221 -1
- package/dist/commands/graph/api-client.d.ts.map +1 -1
- package/dist/commands/graph/api-client.js +141 -2
- package/dist/commands/graph/api-client.js.map +1 -1
- package/dist/commands/graph/load.d.ts.map +1 -1
- package/dist/commands/graph/load.js +40 -2
- package/dist/commands/graph/load.js.map +1 -1
- package/dist/commands/handoff.d.ts +3 -3
- package/dist/commands/handoff.d.ts.map +1 -1
- package/dist/commands/handoff.js +32 -3
- package/dist/commands/handoff.js.map +1 -1
- package/dist/commands/migrate/index.d.ts +27 -0
- package/dist/commands/migrate/index.d.ts.map +1 -0
- package/dist/commands/migrate/index.js +76 -0
- package/dist/commands/migrate/index.js.map +1 -0
- package/dist/commands/migrate/status-migration.d.ts +58 -0
- package/dist/commands/migrate/status-migration.d.ts.map +1 -0
- package/dist/commands/migrate/status-migration.js +323 -0
- package/dist/commands/migrate/status-migration.js.map +1 -0
- package/dist/commands/nudging/index.d.ts +24 -0
- package/dist/commands/nudging/index.d.ts.map +1 -0
- package/dist/commands/nudging/index.js +175 -0
- package/dist/commands/nudging/index.js.map +1 -0
- package/dist/commands/sprint/create.d.ts +26 -0
- package/dist/commands/sprint/create.d.ts.map +1 -0
- package/dist/commands/sprint/create.js +269 -0
- package/dist/commands/sprint/create.js.map +1 -0
- package/dist/commands/sprint/index.d.ts.map +1 -1
- package/dist/commands/sprint/index.js +28 -0
- package/dist/commands/sprint/index.js.map +1 -1
- package/dist/commands/sprint/quick-fix.d.ts +25 -0
- package/dist/commands/sprint/quick-fix.d.ts.map +1 -0
- package/dist/commands/sprint/quick-fix.js +151 -0
- package/dist/commands/sprint/quick-fix.js.map +1 -0
- package/dist/commands/sprint/sprint-pipeline-enhanced.d.ts.map +1 -1
- package/dist/commands/sprint/sprint-pipeline-enhanced.js +37 -0
- package/dist/commands/sprint/sprint-pipeline-enhanced.js.map +1 -1
- package/dist/commands/sprint/status.d.ts +42 -0
- package/dist/commands/sprint/status.d.ts.map +1 -0
- package/dist/commands/sprint/status.js +298 -0
- package/dist/commands/sprint/status.js.map +1 -0
- package/dist/commands/start/start-reflection.d.ts +53 -0
- package/dist/commands/start/start-reflection.d.ts.map +1 -1
- package/dist/commands/start/start-reflection.js +464 -71
- package/dist/commands/start/start-reflection.js.map +1 -1
- package/dist/commands/sync/sprint-syncer.d.ts +19 -12
- package/dist/commands/sync/sprint-syncer.d.ts.map +1 -1
- package/dist/commands/sync/sprint-syncer.js +58 -140
- package/dist/commands/sync/sprint-syncer.js.map +1 -1
- package/dist/commands/sync/sync-command.d.ts.map +1 -1
- package/dist/commands/sync/sync-command.js +6 -18
- package/dist/commands/sync/sync-command.js.map +1 -1
- package/dist/commands/task/index.d.ts +25 -0
- package/dist/commands/task/index.d.ts.map +1 -0
- package/dist/commands/task/index.js +100 -0
- package/dist/commands/task/index.js.map +1 -0
- package/dist/commands/task/status.d.ts +46 -0
- package/dist/commands/task/status.d.ts.map +1 -0
- package/dist/commands/task/status.js +348 -0
- package/dist/commands/task/status.js.map +1 -0
- package/dist/commands/team/index.d.ts +5 -0
- package/dist/commands/team/index.d.ts.map +1 -1
- package/dist/commands/team/index.js +28 -0
- package/dist/commands/team/index.js.map +1 -1
- package/dist/commands/team/status.d.ts +16 -0
- package/dist/commands/team/status.d.ts.map +1 -0
- package/dist/commands/team/status.js +180 -0
- package/dist/commands/team/status.js.map +1 -0
- package/dist/index.js +21 -32
- package/dist/index.js.map +1 -1
- package/dist/lib/adoption-score.d.ts +69 -0
- package/dist/lib/adoption-score.d.ts.map +1 -0
- package/dist/lib/adoption-score.js +206 -0
- package/dist/lib/adoption-score.js.map +1 -0
- package/dist/lib/coaching-level.d.ts +127 -0
- package/dist/lib/coaching-level.d.ts.map +1 -0
- package/dist/lib/coaching-level.js +406 -0
- package/dist/lib/coaching-level.js.map +1 -0
- package/dist/lib/context-loader-events.d.ts.map +1 -1
- package/dist/lib/context-loader-events.js +7 -26
- package/dist/lib/context-loader-events.js.map +1 -1
- package/dist/lib/event-logger.d.ts +42 -0
- package/dist/lib/event-logger.d.ts.map +1 -1
- package/dist/lib/event-logger.js +77 -0
- package/dist/lib/event-logger.js.map +1 -1
- package/dist/lib/output-formatter.d.ts +8 -2
- package/dist/lib/output-formatter.d.ts.map +1 -1
- package/dist/lib/output-formatter.js +98 -18
- package/dist/lib/output-formatter.js.map +1 -1
- package/dist/lib/pending-updates.d.ts +148 -0
- package/dist/lib/pending-updates.d.ts.map +1 -0
- package/dist/lib/pending-updates.js +301 -0
- package/dist/lib/pending-updates.js.map +1 -0
- package/dist/lib/planning-menu.d.ts +69 -0
- package/dist/lib/planning-menu.d.ts.map +1 -0
- package/dist/lib/planning-menu.js +342 -0
- package/dist/lib/planning-menu.js.map +1 -0
- package/dist/lib/sprint-loader.d.ts +86 -14
- package/dist/lib/sprint-loader.d.ts.map +1 -1
- package/dist/lib/sprint-loader.js +293 -98
- package/dist/lib/sprint-loader.js.map +1 -1
- package/dist/lib/state-cache.d.ts +142 -0
- package/dist/lib/state-cache.d.ts.map +1 -0
- package/dist/lib/state-cache.js +259 -0
- package/dist/lib/state-cache.js.map +1 -0
- package/dist/lib/targeted-coaching.d.ts +71 -0
- package/dist/lib/targeted-coaching.d.ts.map +1 -0
- package/dist/lib/targeted-coaching.js +318 -0
- package/dist/lib/targeted-coaching.js.map +1 -0
- package/dist/lib/task-graph-sync.d.ts +105 -0
- package/dist/lib/task-graph-sync.d.ts.map +1 -0
- package/dist/lib/task-graph-sync.js +178 -0
- package/dist/lib/task-graph-sync.js.map +1 -0
- package/dist/lib/task-parser.d.ts +109 -0
- package/dist/lib/task-parser.d.ts.map +1 -0
- package/dist/lib/task-parser.js +407 -0
- package/dist/lib/task-parser.js.map +1 -0
- package/dist/lib/user-sprint.d.ts +53 -0
- package/dist/lib/user-sprint.d.ts.map +1 -1
- package/dist/lib/user-sprint.js +137 -2
- package/dist/lib/user-sprint.js.map +1 -1
- package/dist/lib/work-reconciliation.d.ts +59 -0
- package/dist/lib/work-reconciliation.d.ts.map +1 -0
- package/dist/lib/work-reconciliation.js +267 -0
- package/dist/lib/work-reconciliation.js.map +1 -0
- package/dist/templates/ai-instructions-template.d.ts.map +1 -1
- package/dist/templates/ai-instructions-template.js +7 -5
- package/dist/templates/ai-instructions-template.js.map +1 -1
- package/dist/templates/epic-template.md +0 -2
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +7 -5
- package/dist/types/config.js.map +1 -1
- package/dist/utils/synthesis.d.ts +4 -0
- package/dist/utils/synthesis.d.ts.map +1 -1
- package/dist/utils/synthesis.js +12 -18
- package/dist/utils/synthesis.js.map +1 -1
- package/dist/utils/version-check.d.ts +26 -0
- package/dist/utils/version-check.d.ts.map +1 -0
- package/dist/utils/version-check.js +186 -0
- package/dist/utils/version-check.js.map +1 -0
- 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
|
|
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
|
-
|
|
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
|
-
//
|
|
193
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
//
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
sprintChecklist =
|
|
279
|
-
sprintSource = '
|
|
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
|
-
}
|
|
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
|
-
|
|
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.
|