nightytidy 0.3.8 → 0.3.10

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 (39) hide show
  1. package/bin/nightytidy.js +1 -1
  2. package/package.json +1 -1
  3. package/src/agent/index.js +50 -8
  4. package/src/agent/run-queue.js +12 -0
  5. package/src/agent/service.js +82 -22
  6. package/src/claude.js +1 -1
  7. package/src/prompts/manifest.json +138 -138
  8. package/src/prompts/steps/02-test-coverage.md +181 -181
  9. package/src/prompts/steps/03-test-hardening.md +181 -181
  10. package/src/prompts/steps/04-test-architecture.md +130 -130
  11. package/src/prompts/steps/05-test-consolidation.md +165 -165
  12. package/src/prompts/steps/06-test-quality.md +211 -211
  13. package/src/prompts/steps/07-api-design.md +165 -165
  14. package/src/prompts/steps/08-security-sweep.md +207 -207
  15. package/src/prompts/steps/09-dependency-health.md +217 -217
  16. package/src/prompts/steps/10-codebase-cleanup.md +189 -189
  17. package/src/prompts/steps/11-crosscutting-concerns.md +196 -196
  18. package/src/prompts/steps/12-file-decomposition.md +263 -263
  19. package/src/prompts/steps/13-code-elegance.md +329 -329
  20. package/src/prompts/steps/14-architectural-complexity.md +297 -297
  21. package/src/prompts/steps/15-type-safety.md +192 -192
  22. package/src/prompts/steps/16-logging-error-message.md +173 -173
  23. package/src/prompts/steps/17-data-integrity.md +139 -139
  24. package/src/prompts/steps/18-performance.md +183 -183
  25. package/src/prompts/steps/19-cost-resource-optimization.md +136 -136
  26. package/src/prompts/steps/20-error-recovery.md +145 -145
  27. package/src/prompts/steps/21-race-condition-audit.md +178 -178
  28. package/src/prompts/steps/22-bug-hunt.md +229 -229
  29. package/src/prompts/steps/23-frontend-quality.md +210 -210
  30. package/src/prompts/steps/24-uiux-audit.md +284 -284
  31. package/src/prompts/steps/25-state-management.md +170 -170
  32. package/src/prompts/steps/26-perceived-performance.md +190 -190
  33. package/src/prompts/steps/27-devops.md +165 -165
  34. package/src/prompts/steps/28-scheduled-job-chron-jobs.md +141 -141
  35. package/src/prompts/steps/29-observability.md +152 -152
  36. package/src/prompts/steps/30-backup-check.md +155 -155
  37. package/src/prompts/steps/31-product-polish-ux-friction.md +122 -122
  38. package/src/prompts/steps/32-feature-discovery-opportunity.md +128 -128
  39. package/src/prompts/steps/33-strategic-opportunities.md +217 -217
package/bin/nightytidy.js CHANGED
@@ -1,3 +1,3 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import { run } from '../src/cli.js';
3
3
  run();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nightytidy",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Automated overnight codebase improvement through Claude Code",
5
5
  "license": "MIT",
6
6
  "author": "Dorian Spitz",
@@ -723,6 +723,7 @@ export async function startAgent() {
723
723
  run: { id: run.id, progress: `${stepIndex + 1}/${totalSteps}`, costSoFar: stepData.cost, elapsedMs: stepData.duration },
724
724
  }, project.webhooks);
725
725
  stepIndex++;
726
+ runQueue.updateProgress({ ...runProgress });
726
727
  } else {
727
728
  const errorType = stepParsed.errorType;
728
729
  if (errorType === 'rate_limit') {
@@ -771,6 +772,7 @@ export async function startAgent() {
771
772
  run: { id: run.id },
772
773
  }, project.webhooks);
773
774
  stepIndex++;
775
+ runQueue.updateProgress({ ...runProgress });
774
776
  }
775
777
  }
776
778
 
@@ -959,6 +961,7 @@ export async function startAgent() {
959
961
  project: project.name, projectId: project.id, step: stepData,
960
962
  run: { id: interrupted.id, costSoFar: stepData.cost, elapsedMs: stepData.duration },
961
963
  }, project.webhooks);
964
+ runQueue.updateProgress({ ...runProgress });
962
965
  } else if (stepParsed.errorType === 'rate_limit') {
963
966
  const waitMs = stepParsed.retryAfterMs || 120000;
964
967
  info(` ⏸ Rate limited — waiting ${Math.round(waitMs / 1000)}s`);
@@ -975,6 +978,7 @@ export async function startAgent() {
975
978
  const si = runProgress.stepList.findIndex(s => s.number === stepNum);
976
979
  if (si >= 0) Object.assign(runProgress.stepList[si], { status: 'completed' });
977
980
  wsServer.broadcast({ type: 'step-completed', runId: interrupted.id, step: { number: stepNum, status: 'completed', duration: 0, cost: 0 }, cost: 0 });
981
+ runQueue.updateProgress({ ...runProgress });
978
982
  } else {
979
983
  info(` ✗ Step ${stepNum} failed: ${stepParsed.error || 'unknown'}`);
980
984
  runProgress.failedCount++;
@@ -985,6 +989,7 @@ export async function startAgent() {
985
989
  error: stepParsed.error || stepResult.stderr,
986
990
  });
987
991
  wsServer.broadcast({ type: 'step-failed', runId: interrupted.id, step: { number: stepNum }, error: stepParsed.error || stepResult.stderr });
992
+ runQueue.updateProgress({ ...runProgress });
988
993
  }
989
994
  }
990
995
 
@@ -1228,16 +1233,53 @@ export async function startAgent() {
1228
1233
  }
1229
1234
 
1230
1235
  // Also handle case where queue has a "running" entry from a crash
1231
- // (agent died without graceful shutdown, so markInterrupted was never called)
1236
+ // (agent died without graceful shutdown, so markInterrupted was never called).
1237
+ // Check for progress before discarding — force-kills may have left
1238
+ // lastProgress from per-step updateProgress() calls, or the CLI state
1239
+ // file may still exist in the target project.
1232
1240
  const current = runQueue.getCurrent();
1233
1241
  if (current && current.status === 'running' && !activeBridge) {
1234
- // Orphaned run with no progress — auto-discard instead of blocking the queue
1235
- info(`Found orphaned running run: ${current.id} auto-discarding (0 steps completed)`);
1236
- runQueue.completeCurrent({ success: false });
1237
- dispatchWithQueue('run_failed', {
1238
- projectId: current.projectId,
1239
- run: { id: current.id },
1240
- }, []);
1242
+ const savedProgress = current.lastProgress || {};
1243
+ let completedFromQueue = savedProgress.completedCount || 0;
1244
+
1245
+ // Also check CLI state file as a fallback (belt + suspenders)
1246
+ let completedFromCli = 0;
1247
+ try {
1248
+ const proj = projectManager.getProject(current.projectId);
1249
+ if (proj) {
1250
+ const stateFile = path.join(proj.path, 'nightytidy-run-state.json');
1251
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
1252
+ completedFromCli = (state.completedSteps || []).length;
1253
+ }
1254
+ } catch { /* no state file — fine */ }
1255
+
1256
+ const totalCompleted = Math.max(completedFromQueue, completedFromCli);
1257
+
1258
+ if (totalCompleted === 0) {
1259
+ // No work was done — safe to auto-discard
1260
+ info(`Found orphaned running run: ${current.id} — auto-discarding (0 steps completed)`);
1261
+ runQueue.completeCurrent({ success: false });
1262
+ dispatchWithQueue('run_failed', {
1263
+ projectId: current.projectId,
1264
+ run: { id: current.id },
1265
+ }, []);
1266
+ } else {
1267
+ // Has progress — transition to interrupted so user can resume/finish/discard
1268
+ info(`Found orphaned running run: ${current.id} — transitioning to interrupted (${totalCompleted} steps completed)`);
1269
+ const progress = Object.keys(savedProgress).length > 0
1270
+ ? savedProgress
1271
+ : { completedCount: completedFromCli, failedCount: 0, totalCost: 0 };
1272
+ runQueue.markInterrupted(progress);
1273
+ dispatchWithQueue('run_interrupted', {
1274
+ projectId: current.projectId,
1275
+ run: {
1276
+ id: current.id,
1277
+ totalSteps: current.steps?.length || 0,
1278
+ completedSteps: totalCompleted,
1279
+ elapsedMs: Date.now() - (current.startedAt || Date.now()),
1280
+ },
1281
+ }, []);
1282
+ }
1241
1283
  }
1242
1284
 
1243
1285
  // Process any queued runs left from a previous session
@@ -56,6 +56,18 @@ export class RunQueue {
56
56
  }
57
57
  }
58
58
 
59
+ /**
60
+ * Persist run progress to disk so it survives force-kills and crashes.
61
+ * Called after each step completes/fails. The next startup can read
62
+ * current.lastProgress to recover the run.
63
+ */
64
+ updateProgress(progress) {
65
+ if (this.current) {
66
+ this.current.lastProgress = progress;
67
+ this._save();
68
+ }
69
+ }
70
+
59
71
  /**
60
72
  * Mark the current run as interrupted (agent shutting down mid-run).
61
73
  * Preserves progress data so the run can be resumed on restart.
@@ -88,51 +88,111 @@ export function unregisterService() {
88
88
  // --- Platform implementations ---
89
89
 
90
90
  /**
91
- * Windows: Use the Startup folder (no admin needed).
92
- * Writes a .vbs wrapper script that launches the agent hidden (no console window).
93
- * Falls back to schtasks if Startup folder isn't writable.
91
+ * Windows: Write a .cmd launcher to ~/.nightytidy/ and register it with
92
+ * Task Scheduler (ONLOGON trigger). Task Scheduler is reliable on Windows 11
93
+ * where VBS scripts in the Startup folder are silently blocked by SmartScreen.
94
+ *
95
+ * The .cmd file uses `start /min` to avoid a visible console window.
96
+ * Falls back to the Startup folder VBS approach if schtasks fails.
94
97
  */
95
98
  const STARTUP_DIR = process.platform === 'win32'
96
99
  ? path.join(os.homedir(), 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
97
100
  : '';
98
101
  const STARTUP_VBS = path.join(STARTUP_DIR, 'NightyTidy Agent.vbs');
102
+ const AGENT_CMD = process.platform === 'win32'
103
+ ? path.join(os.homedir(), '.nightytidy', 'start-agent.cmd')
104
+ : '';
99
105
 
100
106
  function _registerWindows(cmd) {
101
- // Primary: Startup folder (no admin required)
107
+ // Write a .cmd launcher script to ~/.nightytidy/start-agent.cmd
108
+ // Avoids quoting issues when passing to Task Scheduler or Startup folder.
109
+ const cmdContent = `@echo off\r\nstart /min "" ${cmd}\r\n`;
110
+ const cmdDir = path.dirname(AGENT_CMD);
111
+ fs.mkdirSync(cmdDir, { recursive: true });
112
+ fs.writeFileSync(AGENT_CMD, cmdContent, 'utf-8');
113
+ debug(`Wrote launcher script: ${AGENT_CMD}`);
114
+
115
+ // Primary: Task Scheduler via PowerShell (no admin required for user-level
116
+ // logon triggers). schtasks.exe /sc onlogon needs admin, but PowerShell's
117
+ // Register-ScheduledTask with a LogonTrigger does not.
118
+ try {
119
+ const escapedPath = AGENT_CMD.replace(/'/g, "''");
120
+ const ps = [
121
+ `$action = New-ScheduledTaskAction -Execute '${escapedPath}'`,
122
+ `$trigger = New-ScheduledTaskTrigger -AtLogOn`,
123
+ `$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable`,
124
+ `Register-ScheduledTask -TaskName '${SERVICE_NAME}' -Action $action -Trigger $trigger -Settings $settings -Force`,
125
+ ].join('; ');
126
+ execSync(`powershell.exe -NoProfile -Command "${ps}"`, { stdio: 'pipe', timeout: 15000 });
127
+ debug('Registered via Task Scheduler (PowerShell)');
128
+ _removeStartupVbs();
129
+ return;
130
+ } catch (err) {
131
+ debug(`Task Scheduler failed: ${err.message}, trying Startup folder`);
132
+ }
133
+
134
+ // Fallback: Startup folder — place .cmd directly (less restricted than .vbs
135
+ // by Windows SmartScreen). The .cmd file runs the agent hidden via start /min.
102
136
  try {
103
- // VBS wrapper runs the agent without showing a console window.
104
- // In VBS, quotes inside strings are doubled: "" not \"
105
- const vbsCmd = cmd.replace(/"/g, '""');
106
- const vbs = `' NightyTidy Agent - auto-start on login\r\nSet ws = CreateObject("WScript.Shell")\r\nws.Run "${vbsCmd}", 0, False\r\n`;
107
137
  fs.mkdirSync(STARTUP_DIR, { recursive: true });
108
- fs.writeFileSync(STARTUP_VBS, vbs, 'utf-8');
109
- debug('Registered via Windows Startup folder');
138
+ const startupCmd = path.join(STARTUP_DIR, 'NightyTidy Agent.cmd');
139
+ fs.copyFileSync(AGENT_CMD, startupCmd);
140
+ debug('Registered via Startup folder (.cmd)');
141
+ _removeStartupVbs();
110
142
  return;
111
143
  } catch (err) {
112
- debug(`Startup folder failed: ${err.message}, trying schtasks`);
144
+ debug(`Startup folder .cmd failed: ${err.message}, trying VBS`);
113
145
  }
114
146
 
115
- // Fallback: Task Scheduler (needs admin on some systems)
116
- execSync(
117
- `schtasks /create /tn "${SERVICE_NAME}" /tr "${cmd}" /sc onlogon /rl LIMITED /f`,
118
- { stdio: 'pipe' },
119
- );
147
+ // Last resort: Startup folder VBS wrapper
148
+ const vbsCmd = cmd.replace(/"/g, '""');
149
+ const vbs = `' NightyTidy Agent - auto-start on login\r\nSet ws = CreateObject("WScript.Shell")\r\nws.Run "${vbsCmd}", 0, False\r\n`;
150
+ fs.mkdirSync(STARTUP_DIR, { recursive: true });
151
+ fs.writeFileSync(STARTUP_VBS, vbs, 'utf-8');
152
+ debug('Registered via Windows Startup folder (VBS)');
120
153
  }
121
154
 
122
- function _unregisterWindows() {
123
- // Remove Startup folder entry
155
+ function _removeStartupVbs() {
124
156
  try {
125
157
  if (fs.existsSync(STARTUP_VBS)) {
126
158
  fs.unlinkSync(STARTUP_VBS);
127
- debug('Removed Startup folder entry');
159
+ debug('Removed stale Startup folder VBS');
160
+ }
161
+ } catch { /* ignore */ }
162
+ }
163
+
164
+ function _unregisterWindows() {
165
+ // Remove Startup folder entries (VBS and .cmd)
166
+ _removeStartupVbs();
167
+ try {
168
+ const startupCmd = path.join(STARTUP_DIR, 'NightyTidy Agent.cmd');
169
+ if (fs.existsSync(startupCmd)) {
170
+ fs.unlinkSync(startupCmd);
171
+ debug('Removed Startup folder .cmd entry');
128
172
  }
129
173
  } catch { /* ignore */ }
130
174
 
131
- // Also try removing schtasks entry (may not exist)
175
+ // Remove .cmd launcher script from ~/.nightytidy/
132
176
  try {
133
- execSync(`schtasks /delete /tn "${SERVICE_NAME}" /f`, { stdio: 'pipe' });
177
+ if (fs.existsSync(AGENT_CMD)) {
178
+ fs.unlinkSync(AGENT_CMD);
179
+ debug('Removed launcher script');
180
+ }
181
+ } catch { /* ignore */ }
182
+
183
+ // Remove Task Scheduler entry (try both PowerShell and schtasks)
184
+ try {
185
+ execSync(
186
+ `powershell.exe -NoProfile -Command "Unregister-ScheduledTask -TaskName '${SERVICE_NAME}' -Confirm:\\$false"`,
187
+ { stdio: 'pipe', timeout: 15000 },
188
+ );
134
189
  debug('Removed Task Scheduler entry');
135
- } catch { /* ignore — may not have been registered via schtasks */ }
190
+ } catch {
191
+ try {
192
+ execSync(`schtasks /delete /tn "${SERVICE_NAME}" /f`, { stdio: 'pipe' });
193
+ debug('Removed Task Scheduler entry (schtasks)');
194
+ } catch { /* ignore — may not have been registered */ }
195
+ }
136
196
  }
137
197
 
138
198
  function _registerMacOS(cmd) {
package/src/claude.js CHANGED
@@ -52,7 +52,7 @@ import { cleanEnv } from './env.js';
52
52
  const DEFAULT_TIMEOUT = 45 * 60 * 1000; // 45 minutes
53
53
  const DEFAULT_RETRIES = 3;
54
54
  const RETRY_DELAY = 10000; // 10 seconds
55
- const STDIN_THRESHOLD = 8000; // chars
55
+ const STDIN_THRESHOLD = 0; // Always pipe via stdin — cmd.exe on Windows corrupts -p prompts with special chars
56
56
  const SIGKILL_DELAY = 5000; // grace period before SIGKILL after initial kill
57
57
  export const INACTIVITY_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes — no stdout or stderr data
58
58
 
@@ -1,138 +1,138 @@
1
- {
2
- "version": 1,
3
- "sourceUrl": "https://docs.google.com/document/d/e/2PACX-1vRtQJyud1t-ESLJqKXTdBGTzFnkxFvZRKJ8_MrOjSGn4fmBluXWVTvJZFIxgSefVag8MoAW8bd0-A6K/pub",
4
- "steps": [
5
- {
6
- "id": "01-documentation",
7
- "name": "Documentation"
8
- },
9
- {
10
- "id": "02-test-coverage",
11
- "name": "Test Coverage"
12
- },
13
- {
14
- "id": "03-test-hardening",
15
- "name": "Test Hardening"
16
- },
17
- {
18
- "id": "04-test-architecture",
19
- "name": "Test Architecture"
20
- },
21
- {
22
- "id": "05-test-consolidation",
23
- "name": "Test Consolidation"
24
- },
25
- {
26
- "id": "06-test-quality",
27
- "name": "Test Quality"
28
- },
29
- {
30
- "id": "07-api-design",
31
- "name": "API Design"
32
- },
33
- {
34
- "id": "08-security-sweep",
35
- "name": "Security Sweep"
36
- },
37
- {
38
- "id": "09-dependency-health",
39
- "name": "Dependency Health"
40
- },
41
- {
42
- "id": "10-codebase-cleanup",
43
- "name": "Codebase Cleanup"
44
- },
45
- {
46
- "id": "11-crosscutting-concerns",
47
- "name": "Cross-Cutting Concerns"
48
- },
49
- {
50
- "id": "12-file-decomposition",
51
- "name": "File Decomposition"
52
- },
53
- {
54
- "id": "13-code-elegance",
55
- "name": "Code Elegance"
56
- },
57
- {
58
- "id": "14-architectural-complexity",
59
- "name": "Architectural Complexity"
60
- },
61
- {
62
- "id": "15-type-safety",
63
- "name": "Type Safety"
64
- },
65
- {
66
- "id": "16-logging-error-message",
67
- "name": "Logging & Error Message"
68
- },
69
- {
70
- "id": "17-data-integrity",
71
- "name": "Data Integrity"
72
- },
73
- {
74
- "id": "18-performance",
75
- "name": "Performance"
76
- },
77
- {
78
- "id": "19-cost-resource-optimization",
79
- "name": "Cost & Resource Optimization"
80
- },
81
- {
82
- "id": "20-error-recovery",
83
- "name": "Error Recovery"
84
- },
85
- {
86
- "id": "21-race-condition-audit",
87
- "name": "Race Condition Audit"
88
- },
89
- {
90
- "id": "22-bug-hunt",
91
- "name": "Bug Hunt"
92
- },
93
- {
94
- "id": "23-frontend-quality",
95
- "name": "Frontend Quality"
96
- },
97
- {
98
- "id": "24-uiux-audit",
99
- "name": "UI/UX Audit"
100
- },
101
- {
102
- "id": "25-state-management",
103
- "name": "State Management"
104
- },
105
- {
106
- "id": "26-perceived-performance",
107
- "name": "Perceived Performance"
108
- },
109
- {
110
- "id": "27-devops",
111
- "name": "DevOps"
112
- },
113
- {
114
- "id": "28-scheduled-job-chron-jobs",
115
- "name": "Scheduled Job & Chron Jobs"
116
- },
117
- {
118
- "id": "29-observability",
119
- "name": "Observability"
120
- },
121
- {
122
- "id": "30-backup-check",
123
- "name": "Backup Check"
124
- },
125
- {
126
- "id": "31-product-polish-ux-friction",
127
- "name": "Product Polish & UX Friction"
128
- },
129
- {
130
- "id": "32-feature-discovery-opportunity",
131
- "name": "Feature Discovery & Opportunity"
132
- },
133
- {
134
- "id": "33-strategic-opportunities",
135
- "name": "Strategic Opportunities"
136
- }
137
- ]
138
- }
1
+ {
2
+ "version": 1,
3
+ "sourceUrl": "https://docs.google.com/document/d/e/2PACX-1vRtQJyud1t-ESLJqKXTdBGTzFnkxFvZRKJ8_MrOjSGn4fmBluXWVTvJZFIxgSefVag8MoAW8bd0-A6K/pub",
4
+ "steps": [
5
+ {
6
+ "id": "01-documentation",
7
+ "name": "Documentation"
8
+ },
9
+ {
10
+ "id": "02-test-coverage",
11
+ "name": "Test Coverage"
12
+ },
13
+ {
14
+ "id": "03-test-hardening",
15
+ "name": "Test Hardening"
16
+ },
17
+ {
18
+ "id": "04-test-architecture",
19
+ "name": "Test Architecture"
20
+ },
21
+ {
22
+ "id": "05-test-consolidation",
23
+ "name": "Test Consolidation"
24
+ },
25
+ {
26
+ "id": "06-test-quality",
27
+ "name": "Test Quality"
28
+ },
29
+ {
30
+ "id": "07-api-design",
31
+ "name": "API Design"
32
+ },
33
+ {
34
+ "id": "08-security-sweep",
35
+ "name": "Security Sweep"
36
+ },
37
+ {
38
+ "id": "09-dependency-health",
39
+ "name": "Dependency Health"
40
+ },
41
+ {
42
+ "id": "10-codebase-cleanup",
43
+ "name": "Codebase Cleanup"
44
+ },
45
+ {
46
+ "id": "11-crosscutting-concerns",
47
+ "name": "Cross-Cutting Concerns"
48
+ },
49
+ {
50
+ "id": "12-file-decomposition",
51
+ "name": "File Decomposition"
52
+ },
53
+ {
54
+ "id": "13-code-elegance",
55
+ "name": "Code Elegance"
56
+ },
57
+ {
58
+ "id": "14-architectural-complexity",
59
+ "name": "Architectural Complexity"
60
+ },
61
+ {
62
+ "id": "15-type-safety",
63
+ "name": "Type Safety"
64
+ },
65
+ {
66
+ "id": "16-logging-error-message",
67
+ "name": "Logging & Error Message"
68
+ },
69
+ {
70
+ "id": "17-data-integrity",
71
+ "name": "Data Integrity"
72
+ },
73
+ {
74
+ "id": "18-performance",
75
+ "name": "Performance"
76
+ },
77
+ {
78
+ "id": "19-cost-resource-optimization",
79
+ "name": "Cost & Resource Optimization"
80
+ },
81
+ {
82
+ "id": "20-error-recovery",
83
+ "name": "Error Recovery"
84
+ },
85
+ {
86
+ "id": "21-race-condition-audit",
87
+ "name": "Race Condition Audit"
88
+ },
89
+ {
90
+ "id": "22-bug-hunt",
91
+ "name": "Bug Hunt"
92
+ },
93
+ {
94
+ "id": "23-frontend-quality",
95
+ "name": "Frontend Quality"
96
+ },
97
+ {
98
+ "id": "24-uiux-audit",
99
+ "name": "UI/UX Audit"
100
+ },
101
+ {
102
+ "id": "25-state-management",
103
+ "name": "State Management"
104
+ },
105
+ {
106
+ "id": "26-perceived-performance",
107
+ "name": "Perceived Performance"
108
+ },
109
+ {
110
+ "id": "27-devops",
111
+ "name": "DevOps"
112
+ },
113
+ {
114
+ "id": "28-scheduled-job-chron-jobs",
115
+ "name": "Scheduled Job & Chron Jobs"
116
+ },
117
+ {
118
+ "id": "29-observability",
119
+ "name": "Observability"
120
+ },
121
+ {
122
+ "id": "30-backup-check",
123
+ "name": "Backup Check"
124
+ },
125
+ {
126
+ "id": "31-product-polish-ux-friction",
127
+ "name": "Product Polish & UX Friction"
128
+ },
129
+ {
130
+ "id": "32-feature-discovery-opportunity",
131
+ "name": "Feature Discovery & Opportunity"
132
+ },
133
+ {
134
+ "id": "33-strategic-opportunities",
135
+ "name": "Strategic Opportunities"
136
+ }
137
+ ]
138
+ }