@ginkoai/cli 2.5.1 → 2.5.2
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/handoff.d.ts +1 -0
- package/dist/commands/handoff.d.ts.map +1 -1
- package/dist/commands/handoff.js +55 -9
- package/dist/commands/handoff.js.map +1 -1
- package/dist/commands/health.js +1 -0
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +42 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pull/pull-command.d.ts.map +1 -1
- package/dist/commands/pull/pull-command.js +10 -0
- package/dist/commands/pull/pull-command.js.map +1 -1
- package/dist/commands/push/push-command.d.ts.map +1 -1
- package/dist/commands/push/push-command.js +10 -0
- package/dist/commands/push/push-command.js.map +1 -1
- package/dist/commands/sprint/index.d.ts.map +1 -1
- package/dist/commands/sprint/index.js +15 -0
- package/dist/commands/sprint/index.js.map +1 -1
- package/dist/commands/sprint/plan-check.d.ts +16 -0
- package/dist/commands/sprint/plan-check.d.ts.map +1 -0
- package/dist/commands/sprint/plan-check.js +122 -0
- package/dist/commands/sprint/plan-check.js.map +1 -0
- package/dist/commands/sprint/status.d.ts +1 -1
- package/dist/commands/sprint/status.d.ts.map +1 -1
- package/dist/commands/sprint/status.js +23 -1
- package/dist/commands/sprint/status.js.map +1 -1
- package/dist/commands/start/start-reflection.d.ts.map +1 -1
- package/dist/commands/start/start-reflection.js +28 -11
- package/dist/commands/start/start-reflection.js.map +1 -1
- package/dist/commands/task/status.d.ts.map +1 -1
- package/dist/commands/task/status.js +228 -0
- package/dist/commands/task/status.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/health-checker.d.ts.map +1 -1
- package/dist/lib/health-checker.js +75 -0
- package/dist/lib/health-checker.js.map +1 -1
- package/dist/lib/integration-warnings.d.ts +72 -0
- package/dist/lib/integration-warnings.d.ts.map +1 -0
- package/dist/lib/integration-warnings.js +273 -0
- package/dist/lib/integration-warnings.js.map +1 -0
- package/dist/lib/output-formatter.d.ts +1 -1
- package/dist/lib/output-formatter.d.ts.map +1 -1
- package/dist/lib/output-formatter.js +31 -1
- package/dist/lib/output-formatter.js.map +1 -1
- package/dist/lib/protected-manifest.d.ts +34 -0
- package/dist/lib/protected-manifest.d.ts.map +1 -0
- package/dist/lib/protected-manifest.js +112 -0
- package/dist/lib/protected-manifest.js.map +1 -0
- package/dist/lib/protection-hook.d.ts +35 -0
- package/dist/lib/protection-hook.d.ts.map +1 -0
- package/dist/lib/protection-hook.js +154 -0
- package/dist/lib/protection-hook.js.map +1 -0
- package/dist/lib/realtime-cursor.js +3 -3
- package/dist/lib/realtime-cursor.js.map +1 -1
- package/dist/lib/session-health.d.ts +75 -0
- package/dist/lib/session-health.d.ts.map +1 -0
- package/dist/lib/session-health.js +252 -0
- package/dist/lib/session-health.js.map +1 -0
- package/dist/lib/sprint-state.d.ts +82 -0
- package/dist/lib/sprint-state.d.ts.map +1 -0
- package/dist/lib/sprint-state.js +338 -0
- package/dist/lib/sprint-state.js.map +1 -0
- package/dist/lib/structural-safeguards.d.ts +26 -0
- package/dist/lib/structural-safeguards.d.ts.map +1 -0
- package/dist/lib/structural-safeguards.js +91 -0
- package/dist/lib/structural-safeguards.js.map +1 -0
- package/dist/lib/task-parser.d.ts +2 -0
- package/dist/lib/task-parser.d.ts.map +1 -1
- package/dist/lib/task-parser.js +30 -0
- package/dist/lib/task-parser.js.map +1 -1
- package/dist/templates/ai-instructions-template.d.ts.map +1 -1
- package/dist/templates/ai-instructions-template.js +8 -1
- package/dist/templates/ai-instructions-template.js.map +1 -1
- package/dist/templates/sprint-template.md +52 -0
- package/package.json +1 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileType: utility
|
|
3
|
+
* @status: current
|
|
4
|
+
* @updated: 2026-03-15
|
|
5
|
+
* @tags: [session-health, context-pressure, degradation, epic-025]
|
|
6
|
+
* @related: [context-metrics.ts, output-formatter.ts, health-checker.ts]
|
|
7
|
+
* @priority: high
|
|
8
|
+
* @complexity: low
|
|
9
|
+
* @dependencies: [fs-extra]
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Session Health Tier Calculation (EPIC-025 Sprint 4)
|
|
13
|
+
*
|
|
14
|
+
* Calculates a health tier from session metrics (message count, duration)
|
|
15
|
+
* to surface context degradation risk to the human partner.
|
|
16
|
+
*
|
|
17
|
+
* The AI partner under cognitive load is the worst judge of its own
|
|
18
|
+
* degradation — EPIC-024 proved this at ~40 messages / ~200K tokens.
|
|
19
|
+
* The human needs the signal.
|
|
20
|
+
*
|
|
21
|
+
* Thresholds calibrated against EPIC-023/024 session data:
|
|
22
|
+
* - EPIC-024: First deployment failure at ~35 messages, serial bug fixing at ~45
|
|
23
|
+
* - EPIC-023: Context score drop at ~30 messages, CLI interop bugs at ~40
|
|
24
|
+
*
|
|
25
|
+
* Compression events: Claude Code does NOT emit hook events for /compact.
|
|
26
|
+
* Message count + duration remain the primary degradation signals.
|
|
27
|
+
*/
|
|
28
|
+
import fs from 'fs-extra';
|
|
29
|
+
import path from 'path';
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Defaults
|
|
32
|
+
// =============================================================================
|
|
33
|
+
/**
|
|
34
|
+
* Default thresholds calibrated against EPIC-023/024 session data.
|
|
35
|
+
*
|
|
36
|
+
* EPIC-024 (262K token session):
|
|
37
|
+
* - Deployment failures began at ~35 messages
|
|
38
|
+
* - Serial bug fixing pattern at ~45 messages
|
|
39
|
+
* - Subagent collision at ~50 messages
|
|
40
|
+
*
|
|
41
|
+
* EPIC-023 (multi-session analysis):
|
|
42
|
+
* - Context score degradation at ~30 messages
|
|
43
|
+
* - CLI interop bugs surfaced at ~40 messages
|
|
44
|
+
* - E2E walkthrough late verification at ~45 messages
|
|
45
|
+
*/
|
|
46
|
+
export const DEFAULT_THRESHOLDS = {
|
|
47
|
+
steadyMessages: 15,
|
|
48
|
+
verifyMessages: 35,
|
|
49
|
+
handoffMessages: 50,
|
|
50
|
+
steadyDurationMin: 60,
|
|
51
|
+
verifyDurationMin: 180,
|
|
52
|
+
handoffDurationMin: 300,
|
|
53
|
+
};
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// Tier Calculation
|
|
56
|
+
// =============================================================================
|
|
57
|
+
/**
|
|
58
|
+
* Calculate health tier from message count and session duration.
|
|
59
|
+
*
|
|
60
|
+
* Uses the higher of message-based or duration-based tier
|
|
61
|
+
* (whichever indicates more pressure).
|
|
62
|
+
*/
|
|
63
|
+
export function calculateHealthTier(messageCount, durationMinutes, thresholds = DEFAULT_THRESHOLDS) {
|
|
64
|
+
// Message-based tier
|
|
65
|
+
let messageTier = 'fresh';
|
|
66
|
+
if (messageCount >= thresholds.handoffMessages) {
|
|
67
|
+
messageTier = 'handoff-recommended';
|
|
68
|
+
}
|
|
69
|
+
else if (messageCount >= thresholds.verifyMessages) {
|
|
70
|
+
messageTier = 'verify-deploys';
|
|
71
|
+
}
|
|
72
|
+
else if (messageCount >= thresholds.steadyMessages) {
|
|
73
|
+
messageTier = 'steady';
|
|
74
|
+
}
|
|
75
|
+
// Duration-based tier
|
|
76
|
+
let durationTier = 'fresh';
|
|
77
|
+
if (durationMinutes >= thresholds.handoffDurationMin) {
|
|
78
|
+
durationTier = 'handoff-recommended';
|
|
79
|
+
}
|
|
80
|
+
else if (durationMinutes >= thresholds.verifyDurationMin) {
|
|
81
|
+
durationTier = 'verify-deploys';
|
|
82
|
+
}
|
|
83
|
+
else if (durationMinutes >= thresholds.steadyDurationMin) {
|
|
84
|
+
durationTier = 'steady';
|
|
85
|
+
}
|
|
86
|
+
// Use the higher (more concerning) tier
|
|
87
|
+
const tierOrder = ['fresh', 'steady', 'verify-deploys', 'handoff-recommended'];
|
|
88
|
+
const messageIdx = tierOrder.indexOf(messageTier);
|
|
89
|
+
const durationIdx = tierOrder.indexOf(durationTier);
|
|
90
|
+
const tier = tierOrder[Math.max(messageIdx, durationIdx)];
|
|
91
|
+
return {
|
|
92
|
+
tier,
|
|
93
|
+
icon: getTierIcon(tier),
|
|
94
|
+
label: getTierLabel(tier),
|
|
95
|
+
messageCount,
|
|
96
|
+
durationMinutes,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function getTierIcon(tier) {
|
|
100
|
+
switch (tier) {
|
|
101
|
+
case 'fresh': return '✅';
|
|
102
|
+
case 'steady': return '◐';
|
|
103
|
+
case 'verify-deploys': return '⚠️';
|
|
104
|
+
case 'handoff-recommended': return '🔄';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function getTierLabel(tier) {
|
|
108
|
+
switch (tier) {
|
|
109
|
+
case 'fresh': return 'fresh';
|
|
110
|
+
case 'steady': return 'steady';
|
|
111
|
+
case 'verify-deploys': return 'verify deploys';
|
|
112
|
+
case 'handoff-recommended': return 'handoff recommended';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// =============================================================================
|
|
116
|
+
// Session Metrics Extraction
|
|
117
|
+
// =============================================================================
|
|
118
|
+
/**
|
|
119
|
+
* Count messages in the current session from events JSONL.
|
|
120
|
+
*/
|
|
121
|
+
export async function getSessionMessageCount(projectRoot) {
|
|
122
|
+
try {
|
|
123
|
+
const root = projectRoot || getProjectRoot();
|
|
124
|
+
const sessionsDir = path.join(root, '.ginko', 'sessions');
|
|
125
|
+
if (!await fs.pathExists(sessionsDir))
|
|
126
|
+
return 0;
|
|
127
|
+
const dirs = await fs.readdir(sessionsDir);
|
|
128
|
+
for (const dir of dirs) {
|
|
129
|
+
const eventsFile = path.join(sessionsDir, dir, 'current-events.jsonl');
|
|
130
|
+
if (await fs.pathExists(eventsFile)) {
|
|
131
|
+
const content = await fs.readFile(eventsFile, 'utf-8');
|
|
132
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
133
|
+
// Count message-type events
|
|
134
|
+
let count = 0;
|
|
135
|
+
for (const line of lines) {
|
|
136
|
+
try {
|
|
137
|
+
const event = JSON.parse(line);
|
|
138
|
+
if (event.type === 'message' || event.type === 'tool_call' ||
|
|
139
|
+
event.type === 'context_score' || event.type === 'task_complete' ||
|
|
140
|
+
event.type === 'task_start') {
|
|
141
|
+
count++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Skip malformed lines
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return count || lines.length;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Metric extraction failure returns 0
|
|
154
|
+
}
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get session duration in minutes from session start timestamp.
|
|
159
|
+
*/
|
|
160
|
+
export async function getSessionDurationMinutes(projectRoot) {
|
|
161
|
+
try {
|
|
162
|
+
const root = projectRoot || getProjectRoot();
|
|
163
|
+
const sessionsDir = path.join(root, '.ginko', 'sessions');
|
|
164
|
+
if (!await fs.pathExists(sessionsDir))
|
|
165
|
+
return 0;
|
|
166
|
+
const dirs = await fs.readdir(sessionsDir);
|
|
167
|
+
for (const dir of dirs) {
|
|
168
|
+
const eventsFile = path.join(sessionsDir, dir, 'current-events.jsonl');
|
|
169
|
+
if (await fs.pathExists(eventsFile)) {
|
|
170
|
+
const content = await fs.readFile(eventsFile, 'utf-8');
|
|
171
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
172
|
+
if (lines.length === 0)
|
|
173
|
+
return 0;
|
|
174
|
+
// Find earliest timestamp
|
|
175
|
+
let earliest = null;
|
|
176
|
+
for (const line of lines) {
|
|
177
|
+
try {
|
|
178
|
+
const event = JSON.parse(line);
|
|
179
|
+
if (event.timestamp) {
|
|
180
|
+
const ts = new Date(event.timestamp).getTime();
|
|
181
|
+
if (!earliest || ts < earliest)
|
|
182
|
+
earliest = ts;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Skip malformed lines
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (earliest) {
|
|
190
|
+
return Math.round((Date.now() - earliest) / 60000);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Duration extraction failure returns 0
|
|
197
|
+
}
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Load custom thresholds from .ginko/config.json if configured.
|
|
202
|
+
*/
|
|
203
|
+
export async function loadThresholds(projectRoot) {
|
|
204
|
+
try {
|
|
205
|
+
const root = projectRoot || getProjectRoot();
|
|
206
|
+
const configFile = path.join(root, '.ginko', 'config.json');
|
|
207
|
+
if (await fs.pathExists(configFile)) {
|
|
208
|
+
const config = await fs.readJson(configFile);
|
|
209
|
+
if (config.healthThresholds) {
|
|
210
|
+
return { ...DEFAULT_THRESHOLDS, ...config.healthThresholds };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Config load failure uses defaults
|
|
216
|
+
}
|
|
217
|
+
return DEFAULT_THRESHOLDS;
|
|
218
|
+
}
|
|
219
|
+
// =============================================================================
|
|
220
|
+
// Convenience
|
|
221
|
+
// =============================================================================
|
|
222
|
+
/**
|
|
223
|
+
* Get current session health tier (all-in-one).
|
|
224
|
+
*/
|
|
225
|
+
export async function getCurrentHealthTier(projectRoot) {
|
|
226
|
+
const [messageCount, durationMinutes, thresholds] = await Promise.all([
|
|
227
|
+
getSessionMessageCount(projectRoot),
|
|
228
|
+
getSessionDurationMinutes(projectRoot),
|
|
229
|
+
loadThresholds(projectRoot),
|
|
230
|
+
]);
|
|
231
|
+
return calculateHealthTier(messageCount, durationMinutes, thresholds);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Format health tier for status line display.
|
|
235
|
+
* Compact format: "12 msgs | ✅ fresh"
|
|
236
|
+
*/
|
|
237
|
+
export function formatHealthForStatusLine(info) {
|
|
238
|
+
return `${info.messageCount} msgs | ${info.icon} ${info.label}`;
|
|
239
|
+
}
|
|
240
|
+
// =============================================================================
|
|
241
|
+
// Helpers
|
|
242
|
+
// =============================================================================
|
|
243
|
+
function getProjectRoot() {
|
|
244
|
+
try {
|
|
245
|
+
const { execSync } = require('child_process');
|
|
246
|
+
return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return process.cwd();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=session-health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-health.js","sourceRoot":"","sources":["../../src/lib/session-health.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileType: utility
|
|
3
|
+
* @status: current
|
|
4
|
+
* @updated: 2026-03-15
|
|
5
|
+
* @tags: [sprint-state, cache, materialization, epic-025]
|
|
6
|
+
* @related: [task-parser.ts, sprint-loader.ts, ../commands/task/status.ts]
|
|
7
|
+
* @priority: high
|
|
8
|
+
* @complexity: medium
|
|
9
|
+
* @dependencies: [fs-extra, path]
|
|
10
|
+
*/
|
|
11
|
+
export interface SprintStateTask {
|
|
12
|
+
title: string;
|
|
13
|
+
status: string;
|
|
14
|
+
knownIssues?: string[];
|
|
15
|
+
blockers?: string[];
|
|
16
|
+
modifiedFiles?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface SprintStateProgress {
|
|
19
|
+
complete: number;
|
|
20
|
+
total: number;
|
|
21
|
+
percentage: number;
|
|
22
|
+
}
|
|
23
|
+
export interface SprintState {
|
|
24
|
+
sprint: string;
|
|
25
|
+
epicId: string;
|
|
26
|
+
epicTitle: string;
|
|
27
|
+
sprintTitle: string;
|
|
28
|
+
progress: SprintStateProgress;
|
|
29
|
+
tasks: Record<string, SprintStateTask>;
|
|
30
|
+
knownIssues: string[];
|
|
31
|
+
blockers: string[];
|
|
32
|
+
lastDeployed: string | null;
|
|
33
|
+
lastUpdated: string;
|
|
34
|
+
lastUpdatedBy: string | null;
|
|
35
|
+
stale: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Materialize sprint state from graph to local cache.
|
|
39
|
+
*
|
|
40
|
+
* Uses getActiveSprint API to fetch current sprint data,
|
|
41
|
+
* then writes structured JSON to .ginko/sprint-state.json.
|
|
42
|
+
*
|
|
43
|
+
* @param projectRoot - Optional project root override
|
|
44
|
+
* @returns The materialized SprintState, or null if no active sprint
|
|
45
|
+
*/
|
|
46
|
+
export declare function materializeSprintState(projectRoot?: string): Promise<SprintState | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Read sprint state from local cache.
|
|
49
|
+
*
|
|
50
|
+
* @param projectRoot - Optional project root override
|
|
51
|
+
* @returns SprintState or null if cache doesn't exist
|
|
52
|
+
*/
|
|
53
|
+
export declare function readSprintState(projectRoot?: string): Promise<SprintState | null>;
|
|
54
|
+
/**
|
|
55
|
+
* Check if cache is stale (>1 hour old).
|
|
56
|
+
*/
|
|
57
|
+
export declare function isCacheStale(state: SprintState, thresholdMs?: number): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Push checkpoint data to a task node in the graph.
|
|
60
|
+
*
|
|
61
|
+
* @param taskId - Task ID
|
|
62
|
+
* @param checkpoint - Checkpoint data (knownIssues, blockers, modifiedFiles)
|
|
63
|
+
*/
|
|
64
|
+
export declare function pushCheckpointToGraph(taskId: string, checkpoint: {
|
|
65
|
+
knownIssues?: string[];
|
|
66
|
+
blockers?: string[];
|
|
67
|
+
modifiedFiles?: string[];
|
|
68
|
+
lastDeployed?: string;
|
|
69
|
+
}): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Get modified files from git since last task complete.
|
|
72
|
+
*/
|
|
73
|
+
export declare function getModifiedFiles(): string[];
|
|
74
|
+
/**
|
|
75
|
+
* Format sprint state for CLI display.
|
|
76
|
+
*/
|
|
77
|
+
export declare function formatSprintState(state: SprintState): string;
|
|
78
|
+
/**
|
|
79
|
+
* Format a compact sprint checkpoint for ginko start readiness message.
|
|
80
|
+
*/
|
|
81
|
+
export declare function formatCheckpointSummary(state: SprintState): string;
|
|
82
|
+
//# sourceMappingURL=sprint-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sprint-state.d.ts","sourceRoot":"","sources":["../../src/lib/sprint-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAsBH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACvC,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAC;CAChB;AAuBD;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAqG7B;AAwBD;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAQ7B;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,WAAW,GAAE,MAAgB,GAAG,OAAO,CAIvF;AAMD;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE;IACV,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GACA,OAAO,CAAC,IAAI,CAAC,CAmCf;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,EAAE,CAa3C;AAMD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAqD5D;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CA0BlE"}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileType: utility
|
|
3
|
+
* @status: current
|
|
4
|
+
* @updated: 2026-03-15
|
|
5
|
+
* @tags: [sprint-state, cache, materialization, epic-025]
|
|
6
|
+
* @related: [task-parser.ts, sprint-loader.ts, ../commands/task/status.ts]
|
|
7
|
+
* @priority: high
|
|
8
|
+
* @complexity: medium
|
|
9
|
+
* @dependencies: [fs-extra, path]
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Sprint State Materialization (EPIC-025 Sprint 2)
|
|
13
|
+
*
|
|
14
|
+
* Materializes graph state as `.ginko/sprint-state.json` — a read-only
|
|
15
|
+
* local cache the AI partner can access with zero friction.
|
|
16
|
+
*
|
|
17
|
+
* Write path: AI partner → `ginko task complete` → CLI → graph → local cache
|
|
18
|
+
* Read path: AI partner → `Read .ginko/sprint-state.json` (one tool call)
|
|
19
|
+
*
|
|
20
|
+
* The AI partner never writes to the cache directly. The CLI is the only writer.
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'fs-extra';
|
|
23
|
+
import path from 'path';
|
|
24
|
+
import { execSync } from 'child_process';
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Helpers
|
|
27
|
+
// =============================================================================
|
|
28
|
+
function getProjectRoot() {
|
|
29
|
+
try {
|
|
30
|
+
return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return process.cwd();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function getCachePath(projectRoot) {
|
|
37
|
+
const root = projectRoot || getProjectRoot();
|
|
38
|
+
return path.join(root, '.ginko', 'sprint-state.json');
|
|
39
|
+
}
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// Materialization
|
|
42
|
+
// =============================================================================
|
|
43
|
+
/**
|
|
44
|
+
* Materialize sprint state from graph to local cache.
|
|
45
|
+
*
|
|
46
|
+
* Uses getActiveSprint API to fetch current sprint data,
|
|
47
|
+
* then writes structured JSON to .ginko/sprint-state.json.
|
|
48
|
+
*
|
|
49
|
+
* @param projectRoot - Optional project root override
|
|
50
|
+
* @returns The materialized SprintState, or null if no active sprint
|
|
51
|
+
*/
|
|
52
|
+
export async function materializeSprintState(projectRoot) {
|
|
53
|
+
const root = projectRoot || getProjectRoot();
|
|
54
|
+
const cachePath = getCachePath(root);
|
|
55
|
+
try {
|
|
56
|
+
// Dynamic import to avoid circular dependencies
|
|
57
|
+
const { GraphApiClient } = await import('../commands/graph/api-client.js');
|
|
58
|
+
const { getGraphId } = await import('../commands/graph/config.js');
|
|
59
|
+
const graphId = process.env.GINKO_GRAPH_ID || await getGraphId();
|
|
60
|
+
if (!graphId)
|
|
61
|
+
return null;
|
|
62
|
+
const client = new GraphApiClient();
|
|
63
|
+
const activeSprint = await client.getActiveSprint(graphId);
|
|
64
|
+
if (!activeSprint?.sprint?.id)
|
|
65
|
+
return null;
|
|
66
|
+
// Build task map
|
|
67
|
+
const tasks = {};
|
|
68
|
+
const allKnownIssues = [];
|
|
69
|
+
const allBlockers = [];
|
|
70
|
+
let completedCount = 0;
|
|
71
|
+
for (const task of activeSprint.tasks || []) {
|
|
72
|
+
const taskEntry = {
|
|
73
|
+
title: task.title || 'Untitled',
|
|
74
|
+
status: task.status || 'not_started',
|
|
75
|
+
};
|
|
76
|
+
// Try to get extended properties (knownIssues, blockers) from graph node
|
|
77
|
+
try {
|
|
78
|
+
const nodeResponse = await client.request('GET', `/api/v1/graph/nodes/${encodeURIComponent(task.id)}?graphId=${encodeURIComponent(graphId)}`);
|
|
79
|
+
const props = nodeResponse.node?.properties;
|
|
80
|
+
if (props) {
|
|
81
|
+
if (props.knownIssues) {
|
|
82
|
+
const issues = Array.isArray(props.knownIssues)
|
|
83
|
+
? props.knownIssues
|
|
84
|
+
: typeof props.knownIssues === 'string'
|
|
85
|
+
? JSON.parse(props.knownIssues)
|
|
86
|
+
: [];
|
|
87
|
+
taskEntry.knownIssues = issues;
|
|
88
|
+
allKnownIssues.push(...issues);
|
|
89
|
+
}
|
|
90
|
+
if (props.blockers) {
|
|
91
|
+
const blockers = Array.isArray(props.blockers)
|
|
92
|
+
? props.blockers
|
|
93
|
+
: typeof props.blockers === 'string'
|
|
94
|
+
? JSON.parse(props.blockers)
|
|
95
|
+
: [];
|
|
96
|
+
taskEntry.blockers = blockers;
|
|
97
|
+
allBlockers.push(...blockers);
|
|
98
|
+
}
|
|
99
|
+
if (props.modifiedFiles) {
|
|
100
|
+
taskEntry.modifiedFiles = Array.isArray(props.modifiedFiles)
|
|
101
|
+
? props.modifiedFiles
|
|
102
|
+
: typeof props.modifiedFiles === 'string'
|
|
103
|
+
? JSON.parse(props.modifiedFiles)
|
|
104
|
+
: [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Individual task property fetch failures are non-fatal
|
|
110
|
+
}
|
|
111
|
+
if (task.status === 'complete')
|
|
112
|
+
completedCount++;
|
|
113
|
+
tasks[task.id] = taskEntry;
|
|
114
|
+
}
|
|
115
|
+
const totalTasks = activeSprint.tasks?.length || 0;
|
|
116
|
+
const state = {
|
|
117
|
+
sprint: activeSprint.sprint.id,
|
|
118
|
+
epicId: activeSprint.sprint.id.split('_')[0] || 'unknown',
|
|
119
|
+
epicTitle: '',
|
|
120
|
+
sprintTitle: activeSprint.sprint.name || activeSprint.sprint.id,
|
|
121
|
+
progress: {
|
|
122
|
+
complete: completedCount,
|
|
123
|
+
total: totalTasks,
|
|
124
|
+
percentage: totalTasks > 0 ? Math.round((completedCount / totalTasks) * 100) : 0,
|
|
125
|
+
},
|
|
126
|
+
tasks,
|
|
127
|
+
knownIssues: [...new Set(allKnownIssues)],
|
|
128
|
+
blockers: [...new Set(allBlockers)],
|
|
129
|
+
lastDeployed: null,
|
|
130
|
+
lastUpdated: new Date().toISOString(),
|
|
131
|
+
lastUpdatedBy: null,
|
|
132
|
+
stale: false,
|
|
133
|
+
};
|
|
134
|
+
// Write cache
|
|
135
|
+
await fs.ensureDir(path.dirname(cachePath));
|
|
136
|
+
await fs.writeJson(cachePath, state, { spaces: 2 });
|
|
137
|
+
return state;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Graph unavailable — try to keep existing cache, mark as stale
|
|
141
|
+
return markCacheStale(cachePath);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Mark existing cache as stale (graph was unavailable).
|
|
146
|
+
* Never deletes the cache — stale data is better than no data.
|
|
147
|
+
*/
|
|
148
|
+
async function markCacheStale(cachePath) {
|
|
149
|
+
try {
|
|
150
|
+
if (await fs.pathExists(cachePath)) {
|
|
151
|
+
const existing = await fs.readJson(cachePath);
|
|
152
|
+
existing.stale = true;
|
|
153
|
+
await fs.writeJson(cachePath, existing, { spaces: 2 });
|
|
154
|
+
return existing;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Can't even read the cache — nothing to do
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// Cache Reading
|
|
164
|
+
// =============================================================================
|
|
165
|
+
/**
|
|
166
|
+
* Read sprint state from local cache.
|
|
167
|
+
*
|
|
168
|
+
* @param projectRoot - Optional project root override
|
|
169
|
+
* @returns SprintState or null if cache doesn't exist
|
|
170
|
+
*/
|
|
171
|
+
export async function readSprintState(projectRoot) {
|
|
172
|
+
const cachePath = getCachePath(projectRoot);
|
|
173
|
+
try {
|
|
174
|
+
if (!await fs.pathExists(cachePath))
|
|
175
|
+
return null;
|
|
176
|
+
return await fs.readJson(cachePath);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Check if cache is stale (>1 hour old).
|
|
184
|
+
*/
|
|
185
|
+
export function isCacheStale(state, thresholdMs = 3600000) {
|
|
186
|
+
if (state.stale)
|
|
187
|
+
return true;
|
|
188
|
+
const age = Date.now() - new Date(state.lastUpdated).getTime();
|
|
189
|
+
return age > thresholdMs;
|
|
190
|
+
}
|
|
191
|
+
// =============================================================================
|
|
192
|
+
// Checkpoint Updates
|
|
193
|
+
// =============================================================================
|
|
194
|
+
/**
|
|
195
|
+
* Push checkpoint data to a task node in the graph.
|
|
196
|
+
*
|
|
197
|
+
* @param taskId - Task ID
|
|
198
|
+
* @param checkpoint - Checkpoint data (knownIssues, blockers, modifiedFiles)
|
|
199
|
+
*/
|
|
200
|
+
export async function pushCheckpointToGraph(taskId, checkpoint) {
|
|
201
|
+
try {
|
|
202
|
+
const { GraphApiClient } = await import('../commands/graph/api-client.js');
|
|
203
|
+
const { getGraphId } = await import('../commands/graph/config.js');
|
|
204
|
+
const graphId = process.env.GINKO_GRAPH_ID || await getGraphId();
|
|
205
|
+
if (!graphId)
|
|
206
|
+
return;
|
|
207
|
+
const client = new GraphApiClient();
|
|
208
|
+
// Update task node with checkpoint properties
|
|
209
|
+
const props = {};
|
|
210
|
+
if (checkpoint.knownIssues?.length) {
|
|
211
|
+
props.knownIssues = JSON.stringify(checkpoint.knownIssues);
|
|
212
|
+
}
|
|
213
|
+
if (checkpoint.blockers?.length) {
|
|
214
|
+
props.blockers = JSON.stringify(checkpoint.blockers);
|
|
215
|
+
}
|
|
216
|
+
if (checkpoint.modifiedFiles?.length) {
|
|
217
|
+
props.modifiedFiles = JSON.stringify(checkpoint.modifiedFiles);
|
|
218
|
+
}
|
|
219
|
+
if (checkpoint.lastDeployed) {
|
|
220
|
+
props.lastDeployed = checkpoint.lastDeployed;
|
|
221
|
+
}
|
|
222
|
+
if (Object.keys(props).length > 0) {
|
|
223
|
+
await client.request('PATCH', `/api/v1/graph/nodes/${encodeURIComponent(taskId)}?graphId=${encodeURIComponent(graphId)}`, props);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Checkpoint push failure is non-fatal — log but don't block
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get modified files from git since last task complete.
|
|
232
|
+
*/
|
|
233
|
+
export function getModifiedFiles() {
|
|
234
|
+
try {
|
|
235
|
+
const diff = execSync('git diff --name-only HEAD', { encoding: 'utf-8' }).trim();
|
|
236
|
+
const untracked = execSync('git ls-files --others --exclude-standard', { encoding: 'utf-8' }).trim();
|
|
237
|
+
const files = new Set();
|
|
238
|
+
if (diff)
|
|
239
|
+
diff.split('\n').forEach(f => files.add(f));
|
|
240
|
+
if (untracked)
|
|
241
|
+
untracked.split('\n').forEach(f => files.add(f));
|
|
242
|
+
return Array.from(files).filter(f => f.length > 0).sort();
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// =============================================================================
|
|
249
|
+
// Formatting
|
|
250
|
+
// =============================================================================
|
|
251
|
+
/**
|
|
252
|
+
* Format sprint state for CLI display.
|
|
253
|
+
*/
|
|
254
|
+
export function formatSprintState(state) {
|
|
255
|
+
const lines = [];
|
|
256
|
+
lines.push(`Sprint: ${state.sprint} — ${state.sprintTitle}`);
|
|
257
|
+
lines.push(`Progress: ${state.progress.percentage}% (${state.progress.complete}/${state.progress.total} tasks complete)`);
|
|
258
|
+
lines.push('');
|
|
259
|
+
// Tasks
|
|
260
|
+
lines.push('Tasks:');
|
|
261
|
+
for (const [id, task] of Object.entries(state.tasks)) {
|
|
262
|
+
const shortId = id.split('_').pop() || id;
|
|
263
|
+
let icon;
|
|
264
|
+
switch (task.status) {
|
|
265
|
+
case 'complete':
|
|
266
|
+
icon = '✅';
|
|
267
|
+
break;
|
|
268
|
+
case 'in_progress':
|
|
269
|
+
icon = '🔄';
|
|
270
|
+
break;
|
|
271
|
+
case 'blocked':
|
|
272
|
+
icon = '⛔';
|
|
273
|
+
break;
|
|
274
|
+
default:
|
|
275
|
+
icon = '⬜';
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
lines.push(` ${icon} ${shortId}: ${task.title}`);
|
|
279
|
+
}
|
|
280
|
+
// Known Issues
|
|
281
|
+
if (state.knownIssues.length > 0) {
|
|
282
|
+
lines.push('');
|
|
283
|
+
lines.push('Known Issues:');
|
|
284
|
+
for (const issue of state.knownIssues) {
|
|
285
|
+
lines.push(` - ${issue}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Blockers
|
|
289
|
+
if (state.blockers.length > 0) {
|
|
290
|
+
lines.push('');
|
|
291
|
+
lines.push('Blockers:');
|
|
292
|
+
for (const blocker of state.blockers) {
|
|
293
|
+
lines.push(` - ${blocker}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
lines.push('');
|
|
298
|
+
lines.push('Blockers: none');
|
|
299
|
+
}
|
|
300
|
+
if (state.lastDeployed) {
|
|
301
|
+
lines.push(`Last deployed: ${state.lastDeployed}`);
|
|
302
|
+
}
|
|
303
|
+
lines.push(`Last updated: ${state.lastUpdated}${state.lastUpdatedBy ? ` (after ${state.lastUpdatedBy})` : ''}`);
|
|
304
|
+
if (state.stale) {
|
|
305
|
+
lines.push('⚠ Cache may be stale — run `ginko pull` to refresh');
|
|
306
|
+
}
|
|
307
|
+
return lines.join('\n');
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Format a compact sprint checkpoint for ginko start readiness message.
|
|
311
|
+
*/
|
|
312
|
+
export function formatCheckpointSummary(state) {
|
|
313
|
+
const lines = [];
|
|
314
|
+
lines.push(`Sprint: ${state.sprint} — ${state.sprintTitle} (${state.progress.percentage}%)`);
|
|
315
|
+
// Find last completed and next task
|
|
316
|
+
let lastCompleted = null;
|
|
317
|
+
let nextTask = null;
|
|
318
|
+
const taskEntries = Object.entries(state.tasks);
|
|
319
|
+
for (const [id, task] of taskEntries) {
|
|
320
|
+
if (task.status === 'complete')
|
|
321
|
+
lastCompleted = `${id.split('_').pop()}: ${task.title}`;
|
|
322
|
+
if (!nextTask && (task.status === 'not_started' || task.status === 'in_progress')) {
|
|
323
|
+
const verb = task.status === 'in_progress' ? 'continue' : 'start';
|
|
324
|
+
nextTask = `${id.split('_').pop()}: ${task.title} (${verb})`;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (lastCompleted)
|
|
328
|
+
lines.push(` Last: ${lastCompleted}`);
|
|
329
|
+
if (state.knownIssues.length > 0) {
|
|
330
|
+
lines.push(` Issues: ${state.knownIssues.join('; ')}`);
|
|
331
|
+
}
|
|
332
|
+
if (nextTask)
|
|
333
|
+
lines.push(` Next: ${nextTask}`);
|
|
334
|
+
if (state.stale)
|
|
335
|
+
lines.push(' ⚠ State may be stale — `ginko pull` to refresh');
|
|
336
|
+
return lines.join('\n');
|
|
337
|
+
}
|
|
338
|
+
//# sourceMappingURL=sprint-state.js.map
|