antigravity-ai-kit 2.1.0 → 3.0.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 (114) hide show
  1. package/.agent/README.md +4 -4
  2. package/.agent/agents/README.md +16 -12
  3. package/.agent/agents/architect.md +1 -0
  4. package/.agent/agents/backend-specialist.md +11 -0
  5. package/.agent/agents/code-reviewer.md +1 -0
  6. package/.agent/agents/database-architect.md +11 -0
  7. package/.agent/agents/devops-engineer.md +11 -0
  8. package/.agent/agents/e2e-runner.md +1 -0
  9. package/.agent/agents/explorer-agent.md +11 -0
  10. package/.agent/agents/frontend-specialist.md +11 -0
  11. package/.agent/agents/mobile-developer.md +11 -0
  12. package/.agent/agents/performance-optimizer.md +11 -0
  13. package/.agent/agents/planner.md +1 -0
  14. package/.agent/agents/refactor-cleaner.md +1 -0
  15. package/.agent/agents/reliability-engineer.md +11 -0
  16. package/.agent/agents/security-reviewer.md +1 -0
  17. package/.agent/agents/sprint-orchestrator.md +10 -0
  18. package/.agent/agents/tdd-guide.md +1 -0
  19. package/.agent/commands/code-review.md +1 -0
  20. package/.agent/commands/debug.md +1 -0
  21. package/.agent/commands/deploy.md +1 -0
  22. package/.agent/commands/help.md +252 -31
  23. package/.agent/commands/plan.md +1 -0
  24. package/.agent/commands/status.md +1 -0
  25. package/.agent/commands/tdd.md +1 -0
  26. package/.agent/contexts/brainstorm.md +26 -0
  27. package/.agent/contexts/debug.md +28 -0
  28. package/.agent/contexts/implement.md +29 -0
  29. package/.agent/contexts/review.md +27 -0
  30. package/.agent/contexts/ship.md +28 -0
  31. package/.agent/engine/identity.json +13 -0
  32. package/.agent/engine/loading-rules.json +23 -1
  33. package/.agent/engine/marketplace-index.json +29 -0
  34. package/.agent/engine/reliability-config.json +14 -0
  35. package/.agent/engine/sdlc-map.json +44 -0
  36. package/.agent/engine/workflow-state.json +28 -2
  37. package/.agent/hooks/hooks.json +27 -25
  38. package/.agent/manifest.json +12 -4
  39. package/.agent/rules.md +2 -1
  40. package/.agent/skills/README.md +10 -5
  41. package/.agent/skills/i18n-localization/SKILL.md +191 -0
  42. package/.agent/skills/mcp-integration/SKILL.md +224 -0
  43. package/.agent/skills/parallel-agents/SKILL.md +1 -1
  44. package/.agent/skills/shell-conventions/SKILL.md +92 -0
  45. package/.agent/skills/ui-ux-pro-max/SKILL.md +557 -0
  46. package/.agent/skills/ui-ux-pro-max/data/charts.csv +26 -0
  47. package/.agent/skills/ui-ux-pro-max/data/colors.csv +97 -0
  48. package/.agent/skills/ui-ux-pro-max/data/icons.csv +101 -0
  49. package/.agent/skills/ui-ux-pro-max/data/landing.csv +31 -0
  50. package/.agent/skills/ui-ux-pro-max/data/products.csv +97 -0
  51. package/.agent/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  52. package/.agent/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  53. package/.agent/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  54. package/.agent/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  55. package/.agent/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  56. package/.agent/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  57. package/.agent/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  58. package/.agent/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  59. package/.agent/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  60. package/.agent/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  61. package/.agent/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  62. package/.agent/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  63. package/.agent/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  64. package/.agent/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  65. package/.agent/skills/ui-ux-pro-max/data/styles.csv +68 -0
  66. package/.agent/skills/ui-ux-pro-max/data/typography.csv +58 -0
  67. package/.agent/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  68. package/.agent/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  69. package/.agent/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  70. package/.agent/skills/ui-ux-pro-max/scripts/core.py +253 -0
  71. package/.agent/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  72. package/.agent/skills/ui-ux-pro-max/scripts/search.py +114 -0
  73. package/.agent/templates/adr-template.md +32 -0
  74. package/.agent/templates/bug-report.md +37 -0
  75. package/.agent/templates/feature-request.md +32 -0
  76. package/.agent/workflows/README.md +92 -78
  77. package/.agent/workflows/brainstorm.md +154 -100
  78. package/.agent/workflows/create.md +142 -75
  79. package/.agent/workflows/debug.md +157 -98
  80. package/.agent/workflows/deploy.md +195 -144
  81. package/.agent/workflows/enhance.md +157 -65
  82. package/.agent/workflows/orchestrate.md +171 -114
  83. package/.agent/workflows/plan.md +147 -72
  84. package/.agent/workflows/preview.md +140 -83
  85. package/.agent/workflows/quality-gate.md +196 -0
  86. package/.agent/workflows/retrospective.md +197 -0
  87. package/.agent/workflows/review.md +188 -0
  88. package/.agent/workflows/status.md +142 -91
  89. package/.agent/workflows/test.md +168 -95
  90. package/.agent/workflows/ui-ux-pro-max.md +181 -127
  91. package/README.md +215 -78
  92. package/bin/ag-kit.js +344 -10
  93. package/lib/agent-registry.js +214 -0
  94. package/lib/agent-reputation.js +351 -0
  95. package/lib/cli-commands.js +235 -0
  96. package/lib/conflict-detector.js +245 -0
  97. package/lib/engineering-manager.js +354 -0
  98. package/lib/error-budget.js +294 -0
  99. package/lib/hook-system.js +252 -0
  100. package/lib/identity.js +245 -0
  101. package/lib/loading-engine.js +208 -0
  102. package/lib/marketplace.js +298 -0
  103. package/lib/plugin-system.js +604 -0
  104. package/lib/security-scanner.js +309 -0
  105. package/lib/self-healing.js +434 -0
  106. package/lib/session-manager.js +261 -0
  107. package/lib/skill-sandbox.js +244 -0
  108. package/lib/task-governance.js +523 -0
  109. package/lib/task-model.js +317 -0
  110. package/lib/updater.js +201 -0
  111. package/lib/verify.js +240 -0
  112. package/lib/workflow-engine.js +353 -0
  113. package/lib/workflow-persistence.js +160 -0
  114. package/package.json +7 -3
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Antigravity AI Kit — Self-Healing Pipeline
3
+ *
4
+ * Detects CI failures, diagnoses root causes, generates
5
+ * JSON fix patches, and applies with confirmation.
6
+ *
7
+ * @module lib/self-healing
8
+ * @author Emre Dursun
9
+ * @since v3.0.0
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const crypto = require('crypto');
17
+
18
+ const AGENT_DIR = '.agent';
19
+ const ENGINE_DIR = 'engine';
20
+ const HEALING_LOG_FILE = 'healing-log.json';
21
+ const LAST_CI_OUTPUT_FILE = 'last-ci-output.txt';
22
+
23
+ /** Maximum healing log entries before pruning */
24
+ const MAX_LOG_ENTRIES = 100;
25
+
26
+ /**
27
+ * @typedef {object} FailureDetection
28
+ * @property {string} type - Failure type: 'test' | 'build' | 'dependency' | 'lint'
29
+ * @property {string} message - Failure message
30
+ * @property {string | null} file - Affected file (if detectable)
31
+ * @property {number | null} line - Line number (if detectable)
32
+ * @property {string} severity - 'critical' | 'high' | 'medium' | 'low'
33
+ */
34
+
35
+ /**
36
+ * @typedef {object} Diagnosis
37
+ * @property {string} category - Root cause: 'syntax' | 'import' | 'type' | 'config' | 'assertion' | 'unknown'
38
+ * @property {string} explanation - Human-readable diagnosis
39
+ * @property {boolean} autoFixable - Whether auto-fix is possible
40
+ */
41
+
42
+ /**
43
+ * @typedef {object} FixPatch
44
+ * @property {string} patchId - Unique patch ID
45
+ * @property {string} file - Target file path
46
+ * @property {'insert' | 'replace' | 'delete'} type - Patch operation type
47
+ * @property {number | null} line - Target line number
48
+ * @property {string} original - Original content (for replace/delete)
49
+ * @property {string} replacement - New content (for insert/replace)
50
+ * @property {'high' | 'medium' | 'low'} confidence - Fix confidence level
51
+ */
52
+
53
+ /**
54
+ * Resolves the healing log file path.
55
+ *
56
+ * @param {string} projectRoot - Root directory
57
+ * @returns {string}
58
+ */
59
+ function resolveHealingLogPath(projectRoot) {
60
+ return path.join(projectRoot, AGENT_DIR, ENGINE_DIR, HEALING_LOG_FILE);
61
+ }
62
+
63
+ /**
64
+ * Loads the healing log from disk.
65
+ *
66
+ * @param {string} projectRoot - Root directory
67
+ * @returns {{ entries: object[] }}
68
+ */
69
+ function loadHealingLog(projectRoot) {
70
+ const filePath = resolveHealingLogPath(projectRoot);
71
+
72
+ if (!fs.existsSync(filePath)) {
73
+ return { entries: [] };
74
+ }
75
+
76
+ try {
77
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
78
+ } catch {
79
+ return { entries: [] };
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Writes the healing log atomically with pruning.
85
+ *
86
+ * @param {string} projectRoot - Root directory
87
+ * @param {{ entries: object[] }} data
88
+ * @returns {void}
89
+ */
90
+ function writeHealingLog(projectRoot, data) {
91
+ const filePath = resolveHealingLogPath(projectRoot);
92
+ const dir = path.dirname(filePath);
93
+
94
+ if (!fs.existsSync(dir)) {
95
+ fs.mkdirSync(dir, { recursive: true });
96
+ }
97
+
98
+ // Prune to last MAX_LOG_ENTRIES
99
+ if (data.entries.length > MAX_LOG_ENTRIES) {
100
+ data.entries = data.entries.slice(-MAX_LOG_ENTRIES);
101
+ }
102
+
103
+ const tempPath = `${filePath}.tmp`;
104
+ fs.writeFileSync(tempPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
105
+ fs.renameSync(tempPath, filePath);
106
+ }
107
+
108
+ // ═══════════════════════════════════════════════════
109
+ // Failure Detection Patterns
110
+ // ═══════════════════════════════════════════════════
111
+
112
+ /** @type {{ pattern: RegExp, type: string, severity: string }[]} */
113
+ const FAILURE_PATTERNS = [
114
+ // Test failures
115
+ {
116
+ pattern: /FAIL\s+([\w./\\-]+)\s*>/,
117
+ type: 'test',
118
+ severity: 'high',
119
+ },
120
+ {
121
+ pattern: /AssertionError:\s*(.+)/,
122
+ type: 'test',
123
+ severity: 'high',
124
+ },
125
+ {
126
+ pattern: /Expected\s*(.+)\s*to\s*(equal|be|match)/i,
127
+ type: 'test',
128
+ severity: 'high',
129
+ },
130
+ // Build failures
131
+ {
132
+ pattern: /error TS(\d+):\s*(.+)/,
133
+ type: 'build',
134
+ severity: 'critical',
135
+ },
136
+ {
137
+ pattern: /SyntaxError:\s*(.+)/,
138
+ type: 'build',
139
+ severity: 'critical',
140
+ },
141
+ // Import/dependency issues
142
+ {
143
+ pattern: /Cannot find module '([^']+)'/,
144
+ type: 'dependency',
145
+ severity: 'high',
146
+ },
147
+ {
148
+ pattern: /Module not found:\s*(.+)/,
149
+ type: 'dependency',
150
+ severity: 'high',
151
+ },
152
+ {
153
+ pattern: /ERR_MODULE_NOT_FOUND/,
154
+ type: 'dependency',
155
+ severity: 'high',
156
+ },
157
+ // Lint errors
158
+ {
159
+ pattern: /(\d+):(\d+)\s+error\s+(.+?)\s+([\w/-]+)$/m,
160
+ type: 'lint',
161
+ severity: 'medium',
162
+ },
163
+ {
164
+ pattern: /eslint.*error/i,
165
+ type: 'lint',
166
+ severity: 'medium',
167
+ },
168
+ ];
169
+
170
+ /**
171
+ * Detects failures from raw CI output text.
172
+ *
173
+ * @param {string} ciOutput - Raw CI log text
174
+ * @returns {FailureDetection[]}
175
+ */
176
+ function detectFailure(ciOutput) {
177
+ if (!ciOutput || typeof ciOutput !== 'string') {
178
+ return [];
179
+ }
180
+
181
+ /** @type {FailureDetection[]} */
182
+ const failures = [];
183
+ const lines = ciOutput.split('\n');
184
+
185
+ for (const line of lines) {
186
+ for (const { pattern, type, severity } of FAILURE_PATTERNS) {
187
+ const match = line.match(pattern);
188
+ if (match) {
189
+ // Try to extract file and line from context
190
+ const fileMatch = line.match(/([\w./\\-]+\.(js|ts|jsx|tsx|json))/);
191
+ const lineMatch = line.match(/:(\d+):/);
192
+
193
+ failures.push({
194
+ type,
195
+ message: match[0].trim(),
196
+ file: fileMatch ? fileMatch[1] : null,
197
+ line: lineMatch ? parseInt(lineMatch[1], 10) : null,
198
+ severity,
199
+ });
200
+ break; // One match per line
201
+ }
202
+ }
203
+ }
204
+
205
+ return failures;
206
+ }
207
+
208
+ // ═══════════════════════════════════════════════════
209
+ // Failure Diagnosis
210
+ // ═══════════════════════════════════════════════════
211
+
212
+ /**
213
+ * Diagnoses the root cause of a detected failure.
214
+ *
215
+ * @param {FailureDetection} failure - Detected failure
216
+ * @returns {Diagnosis}
217
+ */
218
+ function diagnoseFailure(failure) {
219
+ const message = failure.message.toLowerCase();
220
+
221
+ // Import/module issues
222
+ if (failure.type === 'dependency' || message.includes('cannot find module') || message.includes('module not found')) {
223
+ return {
224
+ category: 'import',
225
+ explanation: `Missing module or incorrect import path: ${failure.message}`,
226
+ autoFixable: true,
227
+ };
228
+ }
229
+
230
+ // Syntax errors
231
+ if (failure.type === 'build' && message.includes('syntaxerror')) {
232
+ return {
233
+ category: 'syntax',
234
+ explanation: `Syntax error in source code: ${failure.message}`,
235
+ autoFixable: false,
236
+ };
237
+ }
238
+
239
+ // TypeScript type errors
240
+ if (failure.type === 'build' && message.includes('error ts')) {
241
+ return {
242
+ category: 'type',
243
+ explanation: `TypeScript type error: ${failure.message}`,
244
+ autoFixable: false,
245
+ };
246
+ }
247
+
248
+ // Test assertion failures
249
+ if (failure.type === 'test') {
250
+ return {
251
+ category: 'assertion',
252
+ explanation: `Test assertion failed — requires manual review: ${failure.message}`,
253
+ autoFixable: false,
254
+ };
255
+ }
256
+
257
+ // Lint errors
258
+ if (failure.type === 'lint') {
259
+ return {
260
+ category: 'config',
261
+ explanation: `Lint rule violation: ${failure.message}`,
262
+ autoFixable: true,
263
+ };
264
+ }
265
+
266
+ return {
267
+ category: 'unknown',
268
+ explanation: `Unclassified failure: ${failure.message}`,
269
+ autoFixable: false,
270
+ };
271
+ }
272
+
273
+ // ═══════════════════════════════════════════════════
274
+ // Fix Patch Generation
275
+ // ═══════════════════════════════════════════════════
276
+
277
+ /**
278
+ * Generates a fix patch for a diagnosed failure.
279
+ * Only generates patches for auto-fixable issues.
280
+ *
281
+ * @param {FailureDetection} failure - Detected failure
282
+ * @param {Diagnosis} diagnosis - Diagnosis result
283
+ * @returns {FixPatch | null} Generated patch, or null if not auto-fixable
284
+ */
285
+ function generateFixPatch(failure, diagnosis) {
286
+ if (!diagnosis.autoFixable) {
287
+ return null;
288
+ }
289
+
290
+ const patchId = `HEAL-${crypto.randomUUID().slice(0, 8).toUpperCase()}`;
291
+
292
+ // Missing import → suggest adding import
293
+ if (diagnosis.category === 'import') {
294
+ const moduleMatch = failure.message.match(/(?:Cannot find module|Module not found)[:\s]*'?([^'"\s]+)/i);
295
+ const moduleName = moduleMatch ? moduleMatch[1] : 'unknown-module';
296
+
297
+ return {
298
+ patchId,
299
+ file: failure.file || 'unknown',
300
+ type: 'insert',
301
+ line: 1,
302
+ original: '',
303
+ replacement: `const ${moduleName.replace(/[^a-zA-Z]/g, '')} = require('${moduleName}');`,
304
+ confidence: 'medium',
305
+ };
306
+ }
307
+
308
+ // Lint fix → suggest formatting change
309
+ if (diagnosis.category === 'config') {
310
+ return {
311
+ patchId,
312
+ file: failure.file || 'unknown',
313
+ type: 'replace',
314
+ line: failure.line,
315
+ original: failure.message,
316
+ replacement: `// TODO: Fix lint rule — ${failure.message}`,
317
+ confidence: 'low',
318
+ };
319
+ }
320
+
321
+ return null;
322
+ }
323
+
324
+ /**
325
+ * Applies a fix patch with confirmation safeguard.
326
+ * Dry-run by default — requires explicit opt-in for file writes.
327
+ *
328
+ * @param {string} projectRoot - Root directory
329
+ * @param {FixPatch} patch - Patch to apply
330
+ * @param {object} [options] - Apply options
331
+ * @param {boolean} [options.dryRun] - If true (default), just preview
332
+ * @returns {{ applied: boolean, preview: string, patchId: string }}
333
+ */
334
+ function applyFixWithConfirmation(projectRoot, patch, options = {}) {
335
+ const dryRun = options.dryRun !== false; // Default: true
336
+
337
+ const logEntry = {
338
+ patchId: patch.patchId,
339
+ file: patch.file,
340
+ type: patch.type,
341
+ applied: false,
342
+ dryRun,
343
+ timestamp: new Date().toISOString(),
344
+ rollbackData: {
345
+ original: patch.original,
346
+ line: patch.line,
347
+ },
348
+ };
349
+
350
+ const preview = [
351
+ `Patch: ${patch.patchId}`,
352
+ `File: ${patch.file}`,
353
+ `Type: ${patch.type}`,
354
+ `Line: ${patch.line || 'N/A'}`,
355
+ `Confidence: ${patch.confidence}`,
356
+ `Original: ${patch.original || '(empty)'}`,
357
+ `Replacement: ${patch.replacement}`,
358
+ dryRun ? '[DRY RUN — no changes applied]' : '[APPLIED]',
359
+ ].join('\n');
360
+
361
+ if (!dryRun && patch.file !== 'unknown') {
362
+ const targetPath = path.join(projectRoot, patch.file);
363
+
364
+ if (fs.existsSync(targetPath)) {
365
+ try {
366
+ const content = fs.readFileSync(targetPath, 'utf-8');
367
+ const lines = content.split('\n');
368
+
369
+ if (patch.type === 'insert' && patch.line !== null) {
370
+ const insertIndex = Math.max(0, (patch.line || 1) - 1);
371
+ lines.splice(insertIndex, 0, patch.replacement);
372
+ } else if (patch.type === 'replace' && patch.line !== null) {
373
+ const replaceIndex = (patch.line || 1) - 1;
374
+ if (replaceIndex >= 0 && replaceIndex < lines.length) {
375
+ lines[replaceIndex] = patch.replacement;
376
+ }
377
+ } else if (patch.type === 'delete' && patch.line !== null) {
378
+ const deleteIndex = (patch.line || 1) - 1;
379
+ if (deleteIndex >= 0 && deleteIndex < lines.length) {
380
+ lines.splice(deleteIndex, 1);
381
+ }
382
+ }
383
+
384
+ const tempPath = `${targetPath}.tmp`;
385
+ fs.writeFileSync(tempPath, lines.join('\n'), 'utf-8');
386
+ fs.renameSync(tempPath, targetPath);
387
+
388
+ logEntry.applied = true;
389
+ } catch {
390
+ logEntry.applied = false;
391
+ }
392
+ }
393
+ }
394
+
395
+ // Log the action
396
+ const log = loadHealingLog(projectRoot);
397
+ log.entries.push(logEntry);
398
+ writeHealingLog(projectRoot, log);
399
+
400
+ return {
401
+ applied: logEntry.applied,
402
+ preview,
403
+ patchId: patch.patchId,
404
+ };
405
+ }
406
+
407
+ /**
408
+ * Returns the healing report: recent heal activities and stats.
409
+ *
410
+ * @param {string} projectRoot - Root directory
411
+ * @returns {{ totalHeals: number, successRate: number, recentEntries: object[], pendingPatches: number }}
412
+ */
413
+ function getHealingReport(projectRoot) {
414
+ const log = loadHealingLog(projectRoot);
415
+ const entries = log.entries || [];
416
+
417
+ const applied = entries.filter((e) => e.applied).length;
418
+ const dryRuns = entries.filter((e) => e.dryRun && !e.applied).length;
419
+
420
+ return {
421
+ totalHeals: entries.length,
422
+ successRate: entries.length > 0 ? Math.round((applied / entries.length) * 100) : 0,
423
+ recentEntries: entries.slice(-5),
424
+ pendingPatches: dryRuns,
425
+ };
426
+ }
427
+
428
+ module.exports = {
429
+ detectFailure,
430
+ diagnoseFailure,
431
+ generateFixPatch,
432
+ applyFixWithConfirmation,
433
+ getHealingReport,
434
+ };
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Antigravity AI Kit — Session Manager
3
+ *
4
+ * Automates session-state.json updates so it is no longer
5
+ * a blank template. Tracks active sessions, tasks, and
6
+ * repository state.
7
+ *
8
+ * @module lib/session-manager
9
+ * @author Emre Dursun
10
+ * @since v3.0.0
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const crypto = require('crypto');
18
+ const { execSync } = require('child_process');
19
+
20
+ const AGENT_DIR = '.agent';
21
+ const STATE_FILENAME = 'session-state.json';
22
+
23
+ /**
24
+ * Resolves the absolute path to session-state.json.
25
+ *
26
+ * @param {string} projectRoot - Root directory of the project
27
+ * @returns {string} Absolute path to session-state.json
28
+ */
29
+ function resolveStatePath(projectRoot) {
30
+ return path.join(projectRoot, AGENT_DIR, STATE_FILENAME);
31
+ }
32
+
33
+ /**
34
+ * Loads session state from disk.
35
+ *
36
+ * @param {string} projectRoot - Root directory of the project
37
+ * @returns {object} Parsed session state
38
+ */
39
+ function loadSessionState(projectRoot) {
40
+ const filePath = resolveStatePath(projectRoot);
41
+
42
+ if (!fs.existsSync(filePath)) {
43
+ throw new Error(`Session state file not found: ${filePath}`);
44
+ }
45
+
46
+ const raw = fs.readFileSync(filePath, 'utf-8');
47
+ return JSON.parse(raw);
48
+ }
49
+
50
+ /**
51
+ * Writes session state to disk atomically.
52
+ *
53
+ * @param {string} projectRoot - Root directory of the project
54
+ * @param {object} state - Session state object to write
55
+ * @returns {void}
56
+ */
57
+ function writeSessionState(projectRoot, state) {
58
+ const filePath = resolveStatePath(projectRoot);
59
+ const tempPath = `${filePath}.tmp`;
60
+
61
+ state.lastUpdated = new Date().toISOString();
62
+
63
+ fs.writeFileSync(tempPath, JSON.stringify(state, null, 2) + '\n', 'utf-8');
64
+ fs.renameSync(tempPath, filePath);
65
+ }
66
+
67
+ /**
68
+ * Retrieves current Git branch name safely.
69
+ *
70
+ * @param {string} projectRoot - Root directory of the project
71
+ * @returns {string | null} Branch name or null if not a git repo
72
+ */
73
+ function getGitBranch(projectRoot) {
74
+ try {
75
+ const result = execSync('git rev-parse --abbrev-ref HEAD', {
76
+ cwd: projectRoot,
77
+ encoding: 'utf-8',
78
+ timeout: 5000,
79
+ stdio: ['pipe', 'pipe', 'pipe'],
80
+ });
81
+ return result.trim() || null;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Retrieves the last Git commit SHA safely.
89
+ *
90
+ * @param {string} projectRoot - Root directory of the project
91
+ * @returns {string | null} Commit SHA or null
92
+ */
93
+ function getGitLastCommit(projectRoot) {
94
+ try {
95
+ const result = execSync('git log -1 --format=%H', {
96
+ cwd: projectRoot,
97
+ encoding: 'utf-8',
98
+ timeout: 5000,
99
+ stdio: ['pipe', 'pipe', 'pipe'],
100
+ });
101
+ return result.trim() || null;
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Starts a new session. Generates a session ID, populates
109
+ * date, Git info, and sets status to "active".
110
+ *
111
+ * @param {string} projectRoot - Root directory of the project
112
+ * @param {string} [focus] - Optional session focus description
113
+ * @returns {{ sessionId: string, state: object }}
114
+ */
115
+ function startSession(projectRoot, focus) {
116
+ const state = loadSessionState(projectRoot);
117
+ const sessionId = crypto.randomUUID();
118
+
119
+ state.session = {
120
+ id: sessionId,
121
+ date: new Date().toISOString(),
122
+ focus: focus || null,
123
+ status: 'active',
124
+ };
125
+
126
+ state.repository = {
127
+ currentBranch: getGitBranch(projectRoot),
128
+ lastCommit: getGitLastCommit(projectRoot),
129
+ remoteSynced: false,
130
+ };
131
+
132
+ writeSessionState(projectRoot, state);
133
+
134
+ return { sessionId, state };
135
+ }
136
+
137
+ /**
138
+ * Ends the current session. Sets status to "completed".
139
+ *
140
+ * @param {string} projectRoot - Root directory of the project
141
+ * @returns {{ success: boolean, sessionId: string | null }}
142
+ */
143
+ function endSession(projectRoot) {
144
+ const state = loadSessionState(projectRoot);
145
+
146
+ if (!state.session || !state.session.id) {
147
+ return { success: false, sessionId: null };
148
+ }
149
+
150
+ const sessionId = state.session.id;
151
+ state.session.status = 'completed';
152
+
153
+ // Archive completed tasks count
154
+ state.notes = `Session ${sessionId} completed at ${new Date().toISOString()}`;
155
+
156
+ writeSessionState(projectRoot, state);
157
+
158
+ return { success: true, sessionId };
159
+ }
160
+
161
+ /**
162
+ * Adds a task to the open tasks list.
163
+ *
164
+ * @param {string} projectRoot - Root directory of the project
165
+ * @param {string} title - Task title
166
+ * @param {string} [description] - Optional description
167
+ * @returns {{ taskId: string }}
168
+ */
169
+ function addTask(projectRoot, title, description) {
170
+ const state = loadSessionState(projectRoot);
171
+ const taskId = `TASK-${Date.now().toString(36).toUpperCase()}`;
172
+
173
+ const task = {
174
+ id: taskId,
175
+ title,
176
+ description: description || null,
177
+ createdAt: new Date().toISOString(),
178
+ status: 'open',
179
+ };
180
+
181
+ if (!Array.isArray(state.openTasks)) {
182
+ state.openTasks = [];
183
+ }
184
+
185
+ state.openTasks.push(task);
186
+ state.currentTask = taskId;
187
+
188
+ writeSessionState(projectRoot, state);
189
+
190
+ return { taskId };
191
+ }
192
+
193
+ /**
194
+ * Marks a task as completed, moving it from openTasks to completedTasks.
195
+ *
196
+ * @param {string} projectRoot - Root directory of the project
197
+ * @param {string} taskId - ID of the task to complete
198
+ * @returns {{ success: boolean }}
199
+ */
200
+ function completeTask(projectRoot, taskId) {
201
+ const state = loadSessionState(projectRoot);
202
+
203
+ if (!Array.isArray(state.openTasks)) {
204
+ return { success: false };
205
+ }
206
+
207
+ const taskIndex = state.openTasks.findIndex((task) => task.id === taskId);
208
+
209
+ if (taskIndex === -1) {
210
+ return { success: false };
211
+ }
212
+
213
+ const [task] = state.openTasks.splice(taskIndex, 1);
214
+ task.status = 'completed';
215
+ task.completedAt = new Date().toISOString();
216
+
217
+ if (!Array.isArray(state.completedTasks)) {
218
+ state.completedTasks = [];
219
+ }
220
+
221
+ state.completedTasks.push(task);
222
+
223
+ // Update currentTask
224
+ if (state.currentTask === taskId) {
225
+ state.currentTask = state.openTasks.length > 0 ? state.openTasks[0].id : null;
226
+ }
227
+
228
+ writeSessionState(projectRoot, state);
229
+
230
+ return { success: true };
231
+ }
232
+
233
+ /**
234
+ * Returns a summary of the current session state.
235
+ *
236
+ * @param {string} projectRoot - Root directory of the project
237
+ * @returns {object} Session summary
238
+ */
239
+ function getSessionSummary(projectRoot) {
240
+ const state = loadSessionState(projectRoot);
241
+
242
+ return {
243
+ sessionId: state.session?.id || null,
244
+ status: state.session?.status || 'new',
245
+ focus: state.session?.focus || null,
246
+ branch: state.repository?.currentBranch || null,
247
+ openTaskCount: Array.isArray(state.openTasks) ? state.openTasks.length : 0,
248
+ completedTaskCount: Array.isArray(state.completedTasks) ? state.completedTasks.length : 0,
249
+ currentTask: state.currentTask || null,
250
+ lastUpdated: state.lastUpdated || null,
251
+ };
252
+ }
253
+
254
+ module.exports = {
255
+ loadSessionState,
256
+ startSession,
257
+ endSession,
258
+ addTask,
259
+ completeTask,
260
+ getSessionSummary,
261
+ };