antigravity-ai-kit 3.1.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.agent/agents/planner.md +205 -62
  2. package/.agent/contexts/plan-quality-log.md +30 -0
  3. package/.agent/engine/loading-rules.json +37 -3
  4. package/.agent/hooks/hooks.json +10 -0
  5. package/.agent/manifest.json +4 -3
  6. package/.agent/skills/plan-validation/SKILL.md +192 -0
  7. package/.agent/skills/plan-writing/SKILL.md +47 -8
  8. package/.agent/skills/plan-writing/domain-enhancers.md +114 -0
  9. package/.agent/skills/plan-writing/plan-retrospective.md +116 -0
  10. package/.agent/skills/plan-writing/plan-schema.md +119 -0
  11. package/.agent/workflows/plan.md +49 -5
  12. package/README.md +30 -29
  13. package/bin/ag-kit.js +26 -5
  14. package/lib/agent-registry.js +17 -3
  15. package/lib/agent-reputation.js +3 -11
  16. package/lib/circuit-breaker.js +195 -0
  17. package/lib/cli-commands.js +88 -1
  18. package/lib/config-validator.js +274 -0
  19. package/lib/conflict-detector.js +29 -22
  20. package/lib/constants.js +35 -0
  21. package/lib/engineering-manager.js +9 -27
  22. package/lib/error-budget.js +105 -29
  23. package/lib/hook-system.js +8 -4
  24. package/lib/identity.js +22 -27
  25. package/lib/io.js +74 -0
  26. package/lib/loading-engine.js +248 -35
  27. package/lib/logger.js +118 -0
  28. package/lib/marketplace.js +43 -20
  29. package/lib/plugin-system.js +55 -31
  30. package/lib/plugin-verifier.js +197 -0
  31. package/lib/rate-limiter.js +113 -0
  32. package/lib/security-scanner.js +1 -4
  33. package/lib/self-healing.js +58 -24
  34. package/lib/session-manager.js +51 -48
  35. package/lib/skill-sandbox.js +1 -1
  36. package/lib/task-governance.js +10 -11
  37. package/lib/task-model.js +42 -27
  38. package/lib/updater.js +1 -1
  39. package/lib/verify.js +4 -4
  40. package/lib/workflow-engine.js +88 -68
  41. package/lib/workflow-events.js +166 -0
  42. package/lib/workflow-persistence.js +19 -19
  43. package/package.json +2 -2
@@ -13,6 +13,7 @@
13
13
 
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
+ const { AGENT_DIR, ENGINE_DIR } = require('./constants');
16
17
 
17
18
  // ANSI colors (shared with ag-kit.js)
18
19
  const colors = {
@@ -133,7 +134,7 @@ function healCommand(projectRoot, options = {}) {
133
134
  ciOutput = fs.readFileSync(filePath, 'utf-8');
134
135
  } else {
135
136
  // Try last-saved CI output
136
- const lastCiPath = path.join(projectRoot, '.agent', 'engine', 'last-ci-output.txt');
137
+ const lastCiPath = path.join(projectRoot, AGENT_DIR, ENGINE_DIR, 'last-ci-output.txt');
137
138
  if (fs.existsSync(lastCiPath)) {
138
139
  ciOutput = fs.readFileSync(lastCiPath, 'utf-8');
139
140
  } else {
@@ -228,8 +229,94 @@ function renderDashboardSections(projectRoot) {
228
229
  }
229
230
  }
230
231
 
232
+ /**
233
+ * Health check CLI handler.
234
+ * Aggregates error budget, plugin integrity, config validation, and healing status.
235
+ *
236
+ * @param {string} projectRoot - Root directory
237
+ * @returns {{ healthy: boolean, checks: object[] }}
238
+ */
239
+ function healthCommand(projectRoot) {
240
+ const checks = [];
241
+
242
+ // Error budget health
243
+ try {
244
+ const errorBudget = require('./error-budget');
245
+ const report = errorBudget.getBudgetReport(projectRoot);
246
+ checks.push({
247
+ name: 'Error Budget',
248
+ status: report.status,
249
+ healthy: report.status !== 'EXHAUSTED',
250
+ detail: report.violations.length > 0 ? `Violations: ${report.violations.join(', ')}` : 'All rates within thresholds',
251
+ });
252
+ } catch {
253
+ checks.push({ name: 'Error Budget', status: 'SKIPPED', healthy: true, detail: 'No reliability config found' });
254
+ }
255
+
256
+ // Plugin integrity
257
+ try {
258
+ const pluginVerifier = require('./plugin-verifier');
259
+ const result = pluginVerifier.verifyAllPlugins(projectRoot);
260
+ const pluginHealthy = result.invalid.length === 0;
261
+ checks.push({
262
+ name: 'Plugin Integrity',
263
+ status: pluginHealthy ? 'PASS' : 'FAIL',
264
+ healthy: pluginHealthy,
265
+ detail: `${result.valid} valid, ${result.invalid.length} invalid, ${result.unverified.length} unverified`,
266
+ });
267
+ } catch {
268
+ checks.push({ name: 'Plugin Integrity', status: 'SKIPPED', healthy: true, detail: 'Verifier not available' });
269
+ }
270
+
271
+ // Config validation
272
+ try {
273
+ const configValidator = require('./config-validator');
274
+ const result = configValidator.validateAllConfigs(projectRoot);
275
+ const invalidCount = result.totalConfigs - result.validConfigs;
276
+ const configHealthy = invalidCount === 0;
277
+ checks.push({
278
+ name: 'Config Validation',
279
+ status: configHealthy ? 'PASS' : 'FAIL',
280
+ healthy: configHealthy,
281
+ detail: `${result.validConfigs} valid, ${invalidCount} invalid of ${result.totalConfigs} configs`,
282
+ });
283
+ } catch {
284
+ checks.push({ name: 'Config Validation', status: 'SKIPPED', healthy: true, detail: 'Validator not available' });
285
+ }
286
+
287
+ // Self-healing status
288
+ try {
289
+ const selfHealing = require('./self-healing');
290
+ const report = selfHealing.getHealingReport(projectRoot);
291
+ checks.push({
292
+ name: 'Self-Healing',
293
+ status: report.pendingPatches > 0 ? 'WARNING' : 'PASS',
294
+ healthy: true,
295
+ detail: `${report.totalHeals} heals, ${report.successRate}% success, ${report.pendingPatches} pending`,
296
+ });
297
+ } catch {
298
+ checks.push({ name: 'Self-Healing', status: 'SKIPPED', healthy: true, detail: 'Healer not available' });
299
+ }
300
+
301
+ const healthy = checks.every((c) => c.healthy);
302
+
303
+ // Render output
304
+ console.log(`\n${colors.bright}${colors.blue}═══ Health Check ═══${colors.reset}\n`);
305
+ for (const check of checks) {
306
+ const icon = check.status === 'PASS' || check.status === 'HEALTHY' ? '✓' : check.status === 'FAIL' || check.status === 'EXHAUSTED' ? '✗' : '⚠';
307
+ const color = check.healthy ? 'green' : 'red';
308
+ console.log(` ${colors[color]}${icon} ${check.name}: ${check.status}${colors.reset}`);
309
+ console.log(` ${check.detail}`);
310
+ }
311
+ console.log('');
312
+ console.log(` ${healthy ? `${colors.green}✅ All health checks passed` : `${colors.red}❌ Some health checks failed`}${colors.reset}\n`);
313
+
314
+ return { healthy, checks };
315
+ }
316
+
231
317
  module.exports = {
232
318
  marketCommand,
233
319
  healCommand,
320
+ healthCommand,
234
321
  renderDashboardSections,
235
322
  };
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Antigravity AI Kit — Configuration Validator
3
+ *
4
+ * Runtime JSON schema validation for engine configuration files.
5
+ * Catches configuration corruption and drift before they cause
6
+ * runtime failures.
7
+ *
8
+ * @module lib/config-validator
9
+ * @author Emre Dursun
10
+ * @since v3.2.0
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { AGENT_DIR, ENGINE_DIR, HOOKS_DIR } = require('./constants');
18
+
19
+ /**
20
+ * @typedef {object} ValidationResult
21
+ * @property {boolean} valid - Whether the config is valid
22
+ * @property {string[]} errors - Validation error messages
23
+ * @property {string[]} warnings - Non-critical warnings
24
+ */
25
+
26
+ /**
27
+ * Schema definitions for engine configuration files.
28
+ * Each schema defines required fields, their types, and constraints.
29
+ */
30
+ const SCHEMAS = {
31
+ 'manifest.json': {
32
+ required: ['schemaVersion', 'kitVersion', 'capabilities'],
33
+ types: {
34
+ schemaVersion: 'string',
35
+ kitVersion: 'string',
36
+ capabilities: 'object',
37
+ },
38
+ nested: {
39
+ capabilities: {
40
+ required: ['agents', 'commands', 'skills', 'workflows'],
41
+ types: {
42
+ agents: 'object',
43
+ commands: 'object',
44
+ skills: 'object',
45
+ workflows: 'object',
46
+ },
47
+ },
48
+ },
49
+ },
50
+
51
+ 'workflow-state.json': {
52
+ required: ['currentPhase', 'phases', 'transitions'],
53
+ types: {
54
+ currentPhase: 'string',
55
+ phases: 'object',
56
+ transitions: 'array',
57
+ },
58
+ validators: {
59
+ currentPhase: (value) => {
60
+ const validPhases = ['IDLE', 'EXPLORE', 'PLAN', 'IMPLEMENT', 'VERIFY', 'REVIEW', 'DEPLOY', 'MAINTAIN'];
61
+ return validPhases.includes(value) ? null : `Invalid phase: ${value}. Valid: ${validPhases.join(', ')}`;
62
+ },
63
+ },
64
+ },
65
+
66
+ 'loading-rules.json': {
67
+ required: ['domainRules', 'contextBudget', 'planningMandates'],
68
+ types: {
69
+ domainRules: 'array',
70
+ contextBudget: 'object',
71
+ planningMandates: 'object',
72
+ },
73
+ nested: {
74
+ contextBudget: {
75
+ required: ['maxAgentsPerSession', 'maxSkillsPerSession'],
76
+ types: {
77
+ maxAgentsPerSession: 'number',
78
+ maxSkillsPerSession: 'number',
79
+ },
80
+ },
81
+ planningMandates: {
82
+ required: ['alwaysLoadRules', 'alwaysLoadSkills', 'crossCuttingSections'],
83
+ types: {
84
+ alwaysLoadRules: 'array',
85
+ alwaysLoadSkills: 'array',
86
+ crossCuttingSections: 'array',
87
+ specialistContributors: 'object',
88
+ },
89
+ },
90
+ },
91
+ arrayItemSchema: {
92
+ domainRules: {
93
+ required: ['domain', 'keywords'],
94
+ types: {
95
+ domain: 'string',
96
+ keywords: 'array',
97
+ loadAgents: 'array',
98
+ loadSkills: 'array',
99
+ },
100
+ },
101
+ },
102
+ },
103
+
104
+ 'reliability-config.json': {
105
+ required: ['errorBudget'],
106
+ types: {
107
+ errorBudget: 'object',
108
+ },
109
+ nested: {
110
+ errorBudget: {
111
+ required: ['thresholds', 'resetCadence'],
112
+ types: {
113
+ thresholds: 'object',
114
+ resetCadence: 'string',
115
+ },
116
+ },
117
+ },
118
+ },
119
+
120
+ 'hooks.json': {
121
+ required: ['hooks'],
122
+ types: {
123
+ hooks: 'array',
124
+ },
125
+ arrayItemSchema: {
126
+ hooks: {
127
+ required: ['event'],
128
+ types: {
129
+ event: 'string',
130
+ },
131
+ },
132
+ },
133
+ },
134
+ };
135
+
136
+ /**
137
+ * Validates a value's type.
138
+ *
139
+ * @param {*} value - Value to check
140
+ * @param {string} expectedType - Expected type string
141
+ * @returns {boolean}
142
+ */
143
+ function checkType(value, expectedType) {
144
+ if (expectedType === 'array') {
145
+ return Array.isArray(value);
146
+ }
147
+ return typeof value === expectedType;
148
+ }
149
+
150
+ /**
151
+ * Validates a configuration object against its schema.
152
+ *
153
+ * @param {object} config - Parsed configuration object
154
+ * @param {object} schema - Schema definition
155
+ * @param {string} [prefix=''] - Field path prefix for nested errors
156
+ * @returns {ValidationResult}
157
+ */
158
+ function validateAgainstSchema(config, schema, prefix = '') {
159
+ const errors = [];
160
+ const warnings = [];
161
+
162
+ // Check required fields
163
+ for (const field of (schema.required || [])) {
164
+ const fieldPath = prefix ? `${prefix}.${field}` : field;
165
+ if (config[field] === undefined || config[field] === null) {
166
+ errors.push(`Missing required field: ${fieldPath}`);
167
+ }
168
+ }
169
+
170
+ // Check types
171
+ for (const [field, expectedType] of Object.entries(schema.types || {})) {
172
+ const fieldPath = prefix ? `${prefix}.${field}` : field;
173
+ if (config[field] !== undefined && config[field] !== null) {
174
+ if (!checkType(config[field], expectedType)) {
175
+ errors.push(`Invalid type for ${fieldPath}: expected ${expectedType}, got ${Array.isArray(config[field]) ? 'array' : typeof config[field]}`);
176
+ }
177
+ }
178
+ }
179
+
180
+ // Run custom validators
181
+ for (const [field, validator] of Object.entries(schema.validators || {})) {
182
+ if (config[field] !== undefined) {
183
+ const error = validator(config[field]);
184
+ if (error) {
185
+ errors.push(error);
186
+ }
187
+ }
188
+ }
189
+
190
+ // Validate nested schemas
191
+ for (const [field, nestedSchema] of Object.entries(schema.nested || {})) {
192
+ if (config[field] && typeof config[field] === 'object' && !Array.isArray(config[field])) {
193
+ const nestedResult = validateAgainstSchema(config[field], nestedSchema, prefix ? `${prefix}.${field}` : field);
194
+ errors.push(...nestedResult.errors);
195
+ warnings.push(...nestedResult.warnings);
196
+ }
197
+ }
198
+
199
+ // Validate array items
200
+ for (const [field, itemSchema] of Object.entries(schema.arrayItemSchema || {})) {
201
+ if (Array.isArray(config[field])) {
202
+ config[field].forEach((item, index) => {
203
+ if (typeof item === 'object' && item !== null) {
204
+ const itemResult = validateAgainstSchema(item, itemSchema, `${field}[${index}]`);
205
+ errors.push(...itemResult.errors);
206
+ warnings.push(...itemResult.warnings);
207
+ }
208
+ });
209
+ }
210
+ }
211
+
212
+ return { valid: errors.length === 0, errors, warnings };
213
+ }
214
+
215
+ /**
216
+ * Validates a specific engine configuration file.
217
+ *
218
+ * @param {string} projectRoot - Root directory of the project
219
+ * @param {string} configName - Configuration file name (e.g., 'manifest.json')
220
+ * @returns {ValidationResult}
221
+ */
222
+ function validateConfig(projectRoot, configName) {
223
+ const schema = SCHEMAS[configName];
224
+ if (!schema) {
225
+ return { valid: false, errors: [`No schema defined for: ${configName}`], warnings: [] };
226
+ }
227
+
228
+ const configDir = configName === 'manifest.json'
229
+ ? path.join(projectRoot, AGENT_DIR)
230
+ : configName === 'hooks.json'
231
+ ? path.join(projectRoot, AGENT_DIR, HOOKS_DIR)
232
+ : path.join(projectRoot, AGENT_DIR, ENGINE_DIR);
233
+
234
+ const configPath = path.join(configDir, configName);
235
+
236
+ if (!fs.existsSync(configPath)) {
237
+ return { valid: false, errors: [`Config file not found: ${configPath}`], warnings: [] };
238
+ }
239
+
240
+ let config;
241
+ try {
242
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
243
+ } catch (parseError) {
244
+ return { valid: false, errors: [`Invalid JSON: ${parseError.message}`], warnings: [] };
245
+ }
246
+
247
+ return validateAgainstSchema(config, schema);
248
+ }
249
+
250
+ /**
251
+ * Validates all known engine configuration files.
252
+ *
253
+ * @param {string} projectRoot - Root directory of the project
254
+ * @returns {{ totalConfigs: number, validConfigs: number, results: Object.<string, ValidationResult> }}
255
+ */
256
+ function validateAllConfigs(projectRoot) {
257
+ const entries = Object.keys(SCHEMAS).map(
258
+ (configName) => [configName, validateConfig(projectRoot, configName)]
259
+ );
260
+ const results = Object.fromEntries(entries);
261
+ const validCount = entries.filter(([, result]) => result.valid).length;
262
+
263
+ return {
264
+ totalConfigs: Object.keys(SCHEMAS).length,
265
+ validConfigs: validCount,
266
+ results,
267
+ };
268
+ }
269
+
270
+ module.exports = {
271
+ validateConfig,
272
+ validateAllConfigs,
273
+ SCHEMAS,
274
+ };
@@ -14,8 +14,8 @@
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
16
 
17
- const AGENT_DIR = '.agent';
18
- const ENGINE_DIR = 'engine';
17
+ const { AGENT_DIR, ENGINE_DIR } = require('./constants');
18
+ const { writeJsonAtomic } = require('./io');
19
19
  const FILE_LOCKS_FILE = 'file-locks.json';
20
20
 
21
21
  /** Default lock TTL in milliseconds (30 minutes) */
@@ -65,10 +65,17 @@ function loadLocks(projectRoot) {
65
65
  const now = Date.now();
66
66
 
67
67
  // Filter out expired locks
68
- return locks.filter((lock) => {
68
+ const activeLocks = locks.filter((lock) => {
69
69
  const claimedTime = new Date(lock.claimedAt).getTime();
70
70
  return (now - claimedTime) < (lock.ttlMs || DEFAULT_LOCK_TTL_MS);
71
71
  });
72
+
73
+ // Persist pruned list if stale locks were removed
74
+ if (activeLocks.length < locks.length) {
75
+ writeLocks(projectRoot, activeLocks);
76
+ }
77
+
78
+ return activeLocks;
72
79
  } catch {
73
80
  return [];
74
81
  }
@@ -83,21 +90,13 @@ function loadLocks(projectRoot) {
83
90
  */
84
91
  function writeLocks(projectRoot, locks) {
85
92
  const locksPath = resolveLocksPath(projectRoot);
86
- const tempPath = `${locksPath}.tmp`;
87
- const dir = path.dirname(locksPath);
88
-
89
- if (!fs.existsSync(dir)) {
90
- fs.mkdirSync(dir, { recursive: true });
91
- }
92
-
93
93
  const data = {
94
94
  schemaVersion: '1.0.0',
95
95
  lastUpdated: new Date().toISOString(),
96
96
  locks,
97
97
  };
98
98
 
99
- fs.writeFileSync(tempPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
100
- fs.renameSync(tempPath, locksPath);
99
+ writeJsonAtomic(locksPath, data);
101
100
  }
102
101
 
103
102
  /**
@@ -128,22 +127,30 @@ function claimFile(projectRoot, filePath, agent, ttlMs) {
128
127
  };
129
128
  }
130
129
 
131
- // Update or create lock
130
+ // Update or create lock immutably
132
131
  const existingIndex = locks.findIndex((l) => l.filePath === normalizedPath && l.agent === agent);
132
+ const now = new Date().toISOString();
133
133
 
134
+ let updatedLocks;
134
135
  if (existingIndex !== -1) {
135
- locks[existingIndex].claimedAt = new Date().toISOString();
136
- locks[existingIndex].ttlMs = lockTtl;
136
+ updatedLocks = locks.map((l, i) =>
137
+ i === existingIndex
138
+ ? { ...l, claimedAt: now, ttlMs: lockTtl }
139
+ : l
140
+ );
137
141
  } else {
138
- locks.push({
139
- filePath: normalizedPath,
140
- agent,
141
- claimedAt: new Date().toISOString(),
142
- ttlMs: lockTtl,
143
- });
142
+ updatedLocks = [
143
+ ...locks,
144
+ {
145
+ filePath: normalizedPath,
146
+ agent,
147
+ claimedAt: now,
148
+ ttlMs: lockTtl,
149
+ },
150
+ ];
144
151
  }
145
152
 
146
- writeLocks(projectRoot, locks);
153
+ writeLocks(projectRoot, updatedLocks);
147
154
  return { success: true };
148
155
  }
149
156
 
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Antigravity AI Kit — Shared Constants
3
+ *
4
+ * Central definition of directory names and paths used across
5
+ * all runtime modules. Prevents drift from duplicated strings.
6
+ * Frozen to prevent accidental mutation at runtime.
7
+ *
8
+ * @module lib/constants
9
+ * @since v3.2.0
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ /** Root agent configuration directory name */
15
+ const AGENT_DIR = '.agent';
16
+
17
+ /** Engine subdirectory within .agent */
18
+ const ENGINE_DIR = 'engine';
19
+
20
+ /** Hooks subdirectory within .agent */
21
+ const HOOKS_DIR = 'hooks';
22
+
23
+ /** Skills subdirectory within .agent */
24
+ const SKILLS_DIR = 'skills';
25
+
26
+ /** Plugins subdirectory within .agent */
27
+ const PLUGINS_DIR = 'plugins';
28
+
29
+ module.exports = Object.freeze({
30
+ AGENT_DIR,
31
+ ENGINE_DIR,
32
+ HOOKS_DIR,
33
+ SKILLS_DIR,
34
+ PLUGINS_DIR,
35
+ });
@@ -18,8 +18,8 @@ const taskModel = require('./task-model');
18
18
  const agentRegistry = require('./agent-registry');
19
19
  const agentReputation = require('./agent-reputation');
20
20
 
21
- const AGENT_DIR = '.agent';
22
- const ENGINE_DIR = 'engine';
21
+ const { AGENT_DIR, ENGINE_DIR } = require('./constants');
22
+ const { writeJsonAtomic } = require('./io');
23
23
  const SPRINT_FILE = 'sprint-plans.json';
24
24
 
25
25
  /** Maximum tasks per sprint suggestion */
@@ -82,15 +82,7 @@ function loadSprintData(projectRoot) {
82
82
  */
83
83
  function writeSprintData(projectRoot, data) {
84
84
  const filePath = resolveSprintPath(projectRoot);
85
- const dir = path.dirname(filePath);
86
-
87
- if (!fs.existsSync(dir)) {
88
- fs.mkdirSync(dir, { recursive: true });
89
- }
90
-
91
- const tempPath = `${filePath}.tmp`;
92
- fs.writeFileSync(tempPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
93
- fs.renameSync(tempPath, filePath);
85
+ writeJsonAtomic(filePath, data);
94
86
  }
95
87
 
96
88
  /**
@@ -209,16 +201,11 @@ function generateSprintPlan(projectRoot, options = {}) {
209
201
  tasks = [];
210
202
  }
211
203
 
212
- // Priority sort: critical > high > medium > low
213
- const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
214
- tasks.sort((a, b) => {
215
- const aPriority = priorityOrder[a.priority] ?? 2;
216
- const bPriority = priorityOrder[b.priority] ?? 2;
217
- return aPriority - bPriority;
218
- });
204
+ // Priority sort using shared utility
205
+ const sortedTasks = taskModel.sortByPriority(tasks);
219
206
 
220
207
  // Take top N
221
- const sprintTasks = tasks.slice(0, maxTasks);
208
+ const sprintTasks = sortedTasks.slice(0, maxTasks);
222
209
 
223
210
  // Auto-assign each
224
211
  /** @type {TaskAssignment[]} */
@@ -296,15 +283,10 @@ function suggestNextTask(projectRoot) {
296
283
  return { task: null, reason: 'No open tasks remaining' };
297
284
  }
298
285
 
299
- // Priority sort
300
- const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
301
- openTasks.sort((a, b) => {
302
- const aPriority = priorityOrder[a.priority] ?? 2;
303
- const bPriority = priorityOrder[b.priority] ?? 2;
304
- return aPriority - bPriority;
305
- });
286
+ // Priority sort using shared utility
287
+ const sorted = taskModel.sortByPriority(openTasks);
306
288
 
307
- const topTask = openTasks[0];
289
+ const topTask = sorted[0];
308
290
  return {
309
291
  task: topTask,
310
292
  reason: `Highest priority open task (${topTask.priority})`,