chati-dev 4.0.10 → 4.1.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 (107) hide show
  1. package/README.md +5 -0
  2. package/bin/chati.js +23 -2
  3. package/framework/agents/build/dev.md +43 -10
  4. package/framework/agents/discover/brief.md +38 -8
  5. package/framework/agents/discover/brownfield-wu.md +44 -3
  6. package/framework/agents/discover/greenfield-wu.md +14 -0
  7. package/framework/agents/plan/architect.md +6 -0
  8. package/framework/agents/plan/detail.md +25 -0
  9. package/framework/agents/plan/tasks.md +29 -1
  10. package/framework/agents/plan/ux-brand-architect.md +215 -0
  11. package/framework/agents/plan/ux-component-engineer.md +289 -0
  12. package/framework/agents/plan/ux-researcher.md +166 -0
  13. package/framework/agents/plan/ux.md +126 -1008
  14. package/framework/agents/quality/qa-implementation.md +121 -22
  15. package/framework/agents/quality/qa-planning.md +18 -0
  16. package/framework/config.yaml +15 -4
  17. package/framework/constitution.md +8 -2
  18. package/framework/context/root.md +1 -1
  19. package/framework/data/entity-registry.yaml +59 -3
  20. package/framework/hooks/constitution-guard.js +67 -1
  21. package/framework/hooks/license-guard.js +4 -4
  22. package/framework/hooks/model-governance.js +2 -1
  23. package/framework/hooks/prism-engine.js +74 -6
  24. package/framework/hooks/read-protection.js +1 -1
  25. package/framework/hooks/session-digest.js +159 -7
  26. package/framework/hooks/settings.json +16 -4
  27. package/framework/hooks/style-guard.js +134 -0
  28. package/framework/hooks/undercover-guard.js +220 -0
  29. package/framework/intelligence/context-engine.md +21 -0
  30. package/framework/intelligence/hooks-performance.md +54 -0
  31. package/framework/intelligence/memory-layer.md +47 -0
  32. package/framework/migrations/v4.0-to-v4.1.yaml +165 -0
  33. package/framework/orchestrator/chati.md +327 -1067
  34. package/framework/templates/brandbook-html-tmpl.md +107 -0
  35. package/framework/templates/session-memory-tmpl.yaml +68 -0
  36. package/package.json +1 -1
  37. package/src/autonomy/build-state.js +1 -1
  38. package/src/autonomy/worktree-manager.js +13 -13
  39. package/src/config/agent-customizer.js +1 -1
  40. package/src/config/gemini-hooks-generator.js +6 -6
  41. package/src/config/ide-configs.js +1 -1
  42. package/src/context/bracket-tracker.js +25 -4
  43. package/src/context/engine.js +37 -7
  44. package/src/context/formatter.js +45 -1
  45. package/src/context/index.js +3 -3
  46. package/src/dashboard/data-reader.js +7 -7
  47. package/src/decision/engine.js +2 -2
  48. package/src/decision/registry-healer.js +1 -1
  49. package/src/decision/registry-updater.js +2 -2
  50. package/src/extensions/loader.js +1 -1
  51. package/src/gates/g1-planning-complete.js +1 -1
  52. package/src/gates/g2-qa-planning.js +1 -1
  53. package/src/gates/g3-implementation.js +4 -4
  54. package/src/gates/g4-qa-implementation.js +2 -2
  55. package/src/health/auto-fix.js +3 -3
  56. package/src/health/engine.js +2 -2
  57. package/src/installer/brownfield-upgrader.js +3 -3
  58. package/src/installer/manifest.js +1 -1
  59. package/src/installer/transaction.js +1 -1
  60. package/src/installer/validator.js +2 -2
  61. package/src/intelligence/decision-engine.js +1 -1
  62. package/src/intelligence/memory-manager.js +1 -1
  63. package/src/intelligence/registry-manager.js +2 -2
  64. package/src/intelligence/timeline.js +1 -1
  65. package/src/license/client.js +20 -2
  66. package/src/license/commands.js +7 -7
  67. package/src/memory/agent-memory.js +3 -3
  68. package/src/memory/daily-digest.js +170 -0
  69. package/src/memory/dream.js +254 -0
  70. package/src/memory/gotchas.js +2 -2
  71. package/src/memory/index.js +18 -0
  72. package/src/memory/magic-docs.js +98 -0
  73. package/src/memory/memory-extractor.js +163 -0
  74. package/src/memory/session-digest.js +144 -6
  75. package/src/merger/yaml-merger.js +1 -1
  76. package/src/orchestrator/cli.js +980 -0
  77. package/src/orchestrator/handoff-engine.js +25 -4
  78. package/src/orchestrator/index.js +9 -0
  79. package/src/orchestrator/session-manager.js +1 -1
  80. package/src/preview/detector.js +3 -3
  81. package/src/preview/launcher.js +2 -2
  82. package/src/quality/metrics-collector.js +1 -1
  83. package/src/quality/test-runner.js +2 -4
  84. package/src/scanning/density-scanner.js +51 -0
  85. package/src/scanning/env-scanner.js +97 -0
  86. package/src/scanning/index.js +7 -0
  87. package/src/scanning/leakage-scanner.js +54 -0
  88. package/src/scanning/placeholder-scanner.js +40 -0
  89. package/src/scanning/security-scanner.js +94 -0
  90. package/src/security/bash-security.js +335 -0
  91. package/src/security/index.js +9 -0
  92. package/src/telemetry/config.js +3 -5
  93. package/src/telemetry/sender.js +19 -17
  94. package/src/terminal/cli-registry.js +1 -1
  95. package/src/terminal/cost-tracker.js +1 -2
  96. package/src/terminal/prompt-builder.js +11 -27
  97. package/src/terminal/rate-limiter.js +1 -2
  98. package/src/terminal/run-agent.js +3 -3
  99. package/src/terminal/spawner.js +41 -4
  100. package/src/upgrade/checker.js +2 -2
  101. package/src/upgrade/migrator.js +1 -1
  102. package/src/utils/config-parser.js +1 -1
  103. package/src/utils/file-lock.js +3 -3
  104. package/src/utils/provider-limits.js +35 -4
  105. package/src/wizard/i18n.js +2 -2
  106. package/src/wizard/index.js +2 -1
  107. package/src/wizard/questions.js +4 -2
@@ -92,7 +92,7 @@ export function discoverExtensions(projectDir) {
92
92
  .map(f => join(extDir, f));
93
93
 
94
94
  return { files: jsFiles, dir: extDir };
95
- } catch {
95
+ } catch { /* expected: directory may not exist */
96
96
  return { files: [], dir: extDir };
97
97
  }
98
98
  }
@@ -54,7 +54,7 @@ export class PlanningCompleteGate extends GateBase {
54
54
  try {
55
55
  const entries = readdirSync(dirPath);
56
56
  hasContent = entries.length > 0;
57
- } catch {
57
+ } catch { /* expected: directory may not exist */
58
58
  hasContent = false;
59
59
  }
60
60
  }
@@ -56,7 +56,7 @@ export class QAPlanningGate extends GateBase {
56
56
  evidence.riskAssessment = true;
57
57
  }
58
58
  }
59
- } catch {
59
+ } catch { /* expected: operation may fail gracefully */
60
60
  // Directory read failed, leave as false
61
61
  }
62
62
  }
@@ -89,7 +89,7 @@ export class ImplementationGate extends GateBase {
89
89
  } else {
90
90
  evidence.lintPasses = null; // Ambiguous — file exists but no clear verdict
91
91
  }
92
- } catch {
92
+ } catch { /* expected: file may not exist */
93
93
  evidence.lintPasses = null;
94
94
  }
95
95
  }
@@ -112,7 +112,7 @@ export class ImplementationGate extends GateBase {
112
112
  } else {
113
113
  evidence.securityClean = null;
114
114
  }
115
- } catch {
115
+ } catch { /* expected: file may not exist */
116
116
  evidence.securityClean = null;
117
117
  }
118
118
  break;
@@ -126,7 +126,7 @@ export class ImplementationGate extends GateBase {
126
126
  try {
127
127
  const entries = readdirSync(srcDir, { recursive: false });
128
128
  evidence.sourceFilesCreated = entries.length > 0;
129
- } catch {
129
+ } catch { /* expected: directory may not exist */
130
130
  // Ignore
131
131
  }
132
132
  }
@@ -138,7 +138,7 @@ export class ImplementationGate extends GateBase {
138
138
  try {
139
139
  const entries = readdirSync(testDir, { recursive: false });
140
140
  evidence.testFilesCreated = entries.length > 0;
141
- } catch {
141
+ } catch { /* expected: directory may not exist */
142
142
  // Ignore
143
143
  }
144
144
  }
@@ -87,7 +87,7 @@ export class QAImplementationGate extends GateBase {
87
87
  } else {
88
88
  evidence.performanceBenchmarksMet = null; // Ambiguous
89
89
  }
90
- } catch {
90
+ } catch { /* expected: file may not exist */
91
91
  evidence.performanceBenchmarksMet = null;
92
92
  }
93
93
  break;
@@ -114,7 +114,7 @@ export class QAImplementationGate extends GateBase {
114
114
  // File exists but has no clear pass/fail indicators — ambiguous — not clean by default
115
115
  evidence.securityScanClean = null;
116
116
  }
117
- } catch {
117
+ } catch { /* expected: operation may fail gracefully */
118
118
  // File exists but cannot be read — treat as not available
119
119
  evidence.securityScanClean = null;
120
120
  }
@@ -129,7 +129,7 @@ function fixStaleLocks(projectDir) {
129
129
  actions.push({ action: `Failed to check lock ${lockFile}`, success: false, error: err.message });
130
130
  }
131
131
  }
132
- } catch {
132
+ } catch { /* expected: path may already be removed */
133
133
  // Directory read failed — skip
134
134
  }
135
135
 
@@ -199,11 +199,11 @@ function fixOrphanedWorktrees(projectDir) {
199
199
  actions.push({ action: `Removed empty worktree dir: ${entry}`, success: true });
200
200
  }
201
201
  }
202
- } catch {
202
+ } catch { /* expected: directory may not exist */
203
203
  // Skip entries we can't read
204
204
  }
205
205
  }
206
- } catch {
206
+ } catch { /* expected: directory may not exist */
207
207
  // Directory read failed
208
208
  }
209
209
 
@@ -167,14 +167,14 @@ export async function checkDependencies() {
167
167
  if (major < 20) {
168
168
  issues.push(`Node.js ${nodeVersion} < required v20`);
169
169
  }
170
- } catch {
170
+ } catch { /* expected: content may be malformed */
171
171
  issues.push('Node.js not found');
172
172
  }
173
173
 
174
174
  // Check git
175
175
  try {
176
176
  execSync('git --version', { stdio: 'ignore' });
177
- } catch {
177
+ } catch { /* expected: git command may fail */
178
178
  issues.push('git not found');
179
179
  }
180
180
 
@@ -186,8 +186,8 @@ export function upgradeInstallation(targetDir, newFrameworkDir, newVersion = '0.
186
186
  result.preserved.push(filePath);
187
187
  result.skipped.push(filePath);
188
188
  }
189
- } catch {
190
- // Merge failed => preserve user's version
189
+ } catch (err) {
190
+ process.stderr.write(`[chati] upgrader smart-merge: ${err.message}\n`);
191
191
  result.preserved.push(filePath);
192
192
  result.skipped.push(filePath);
193
193
  }
@@ -231,7 +231,7 @@ function collectRelativePaths(rootDir, base = '') {
231
231
  let entries;
232
232
  try {
233
233
  entries = readdirSync(join(rootDir, base), { withFileTypes: true });
234
- } catch {
234
+ } catch { /* expected: directory may not exist */
235
235
  return paths;
236
236
  }
237
237
 
@@ -16,7 +16,7 @@ let SIGNING_PUBLIC_KEY = null;
16
16
  try {
17
17
  const pemPath = join(__dirname, 'signing-public-key.pem');
18
18
  SIGNING_PUBLIC_KEY = createPublicKey(readFileSync(pemPath));
19
- } catch {
19
+ } catch { /* expected: file may not exist */
20
20
  // Public key not available (dev environment without key generation)
21
21
  }
22
22
 
@@ -203,7 +203,7 @@ export class InstallTransaction {
203
203
  try {
204
204
  rmSync(reverseOp.absPath, { recursive: true, force: true });
205
205
  rolledBack++;
206
- } catch {
206
+ } catch { /* expected: path may already be removed */
207
207
  // Directory may not be empty due to other changes; skip
208
208
  }
209
209
  }
@@ -222,8 +222,8 @@ export async function validateInstallation(targetDir) {
222
222
 
223
223
  results.context.details.push({ overlays: overlayDetails });
224
224
  }
225
- } catch {
226
- // Config parsing is best-effort for validation
225
+ } catch (err) {
226
+ process.stderr.write(`[chati] validator config-parse: ${err.message}\n`);
227
227
  }
228
228
  }
229
229
 
@@ -179,7 +179,7 @@ export function loadRegistryEntities(registryPath) {
179
179
  try {
180
180
  const raw = readFileSync(registryPath, 'utf-8');
181
181
  return parseRegistryEntities(raw);
182
- } catch {
182
+ } catch { /* expected: file may not exist */
183
183
  return [];
184
184
  }
185
185
  }
@@ -182,7 +182,7 @@ export function parseMemoryFrontmatter(filePath) {
182
182
  }
183
183
 
184
184
  return meta;
185
- } catch {
185
+ } catch { /* expected: operation may fail gracefully */
186
186
  return null;
187
187
  }
188
188
  }
@@ -118,7 +118,7 @@ export function runHealthCheck(targetDir) {
118
118
  try {
119
119
  JSON.parse(readFileSync(schemaPath, 'utf-8'));
120
120
  validSchemas++;
121
- } catch {
121
+ } catch { /* expected: file may not exist */
122
122
  // Invalid JSON
123
123
  }
124
124
  }
@@ -176,7 +176,7 @@ function loadRegistry(registryPath) {
176
176
  try {
177
177
  const content = readFileSync(registryPath, 'utf-8');
178
178
  return yaml.load(content);
179
- } catch {
179
+ } catch { /* expected: file may not exist */
180
180
  return null;
181
181
  }
182
182
  }
@@ -54,7 +54,7 @@ export function loadTimeline(projectDir) {
54
54
  if (!existsSync(timelinePath)) return [];
55
55
  try {
56
56
  return JSON.parse(readFileSync(timelinePath, 'utf-8'));
57
- } catch {
57
+ } catch { /* expected: file may not exist */
58
58
  return [];
59
59
  }
60
60
  }
@@ -125,10 +125,28 @@ export async function validateLicense(_projectDir) {
125
125
  }
126
126
 
127
127
  /**
128
- * Remove license locally (does not deactivate machine slot on server).
128
+ * Remove license locally and deactivate machine slot on server (best-effort).
129
+ * Fails silently if API is unreachable.
129
130
  */
130
- export function deactivateLicense(_projectDir) {
131
+ export async function deactivateLicense(_projectDir) {
131
132
  const data = readGlobal();
133
+ const key = data.key && data.key !== 'null' ? data.key : null;
134
+
135
+ // Best-effort server deactivation (key + machine_id = proof of ownership)
136
+ if (key) {
137
+ try {
138
+ const machineId = getMachineId();
139
+ await fetch(`${API_BASE}/license/self-deactivate`, {
140
+ method: 'POST',
141
+ headers: { 'Content-Type': 'application/json' },
142
+ body: JSON.stringify({ key, machine_id: machineId }),
143
+ signal: AbortSignal.timeout(5000),
144
+ });
145
+ } catch { /* expected: network may be unavailable */
146
+ // Fail silently — local deactivation still proceeds
147
+ }
148
+ }
149
+
132
150
  writeGlobal({
133
151
  ...data,
134
152
  key: null,
@@ -50,15 +50,15 @@ export async function runActivate(projectDir, keyArg) {
50
50
  // deactivate
51
51
  // ---------------------------------------------------------------------------
52
52
 
53
- export function runDeactivate(projectDir) {
53
+ export async function runDeactivate(projectDir) {
54
54
  const key = getLicenseKey(projectDir);
55
55
  if (!key) {
56
56
  console.log('No license key found locally. Nothing to remove.');
57
57
  return;
58
58
  }
59
- deactivateLicense(projectDir);
60
- console.log(`\n✓ License key removed locally.`);
61
- console.log(` To free up a machine slot, visit https://chati.dev/dashboard\n`);
59
+ console.log('Deactivating...');
60
+ await deactivateLicense(projectDir);
61
+ console.log(`\n✓ License key removed and machine slot freed.\n`);
62
62
  }
63
63
 
64
64
  // ---------------------------------------------------------------------------
@@ -74,13 +74,13 @@ export async function runLicenseStatus(projectDir) {
74
74
  return;
75
75
  }
76
76
 
77
- console.log(`\nChecking license...`);
77
+ console.log(`\nChecking license (refreshing cache)...`);
78
78
 
79
79
  let status;
80
80
  try {
81
81
  status = await validateLicense(projectDir);
82
- } catch {
83
- // Fallback to cache
82
+ } catch (err) {
83
+ process.stderr.write(`[chati] license validate: ${err.message}\n`);
84
84
  status = getLicenseStatus(projectDir) ?? { status: 'UNKNOWN' };
85
85
  }
86
86
 
@@ -123,7 +123,7 @@ export function readAgentMemory(projectDir, agentName) {
123
123
  entries,
124
124
  raw,
125
125
  };
126
- } catch {
126
+ } catch { /* expected: file may not exist */
127
127
  return {
128
128
  loaded: false,
129
129
  entries: [],
@@ -208,7 +208,7 @@ export function searchAgentMemories(projectDir, query) {
208
208
  }
209
209
  });
210
210
  }
211
- } catch {
211
+ } catch { /* expected: operation may fail gracefully */
212
212
  return [];
213
213
  }
214
214
 
@@ -267,7 +267,7 @@ export function getAgentMemoryStats(projectDir) {
267
267
  categories: [...new Set(memory.entries.map(e => e.category))],
268
268
  };
269
269
  }
270
- } catch {
270
+ } catch { /* expected: operation may fail gracefully */
271
271
  return { byAgent: {} };
272
272
  }
273
273
 
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Daily Digest — KAIROS Lite session summary system.
3
+ *
4
+ * Inspired by Claude Code's KAIROS daemon (append-only daily logs),
5
+ * but implemented as a lightweight non-daemon approach that generates
6
+ * daily summaries on demand or at session end.
7
+ *
8
+ * Creates daily digest files at .chati/memories/shared/daily/YYYY-MM-DD.md
9
+ * with cumulative session activity: tasks completed/failed, decisions,
10
+ * gotchas, agent scores, and timing.
11
+ */
12
+
13
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
14
+ import { join } from 'path';
15
+
16
+ const DAILY_DIR = '.chati/memories/shared/daily';
17
+
18
+ /**
19
+ * Build a daily digest entry from the current session state.
20
+ *
21
+ * @param {string} projectDir - Project root
22
+ * @param {object} sessionState - Current session.yaml data
23
+ * @param {object} [extras={}] - Additional context
24
+ * @param {string[]} [extras.decisions] - Decisions made
25
+ * @param {string[]} [extras.gotchas] - Gotchas found
26
+ * @param {number} [extras.duration] - Session duration in minutes
27
+ * @returns {object} Digest entry
28
+ */
29
+ export function buildDailyEntry(projectDir, sessionState, extras = {}) {
30
+ const now = new Date();
31
+ const agents = sessionState.agents || {};
32
+
33
+ // Compute agent stats
34
+ const completed = [];
35
+ const failed = [];
36
+ const scores = {};
37
+
38
+ for (const [name, data] of Object.entries(agents)) {
39
+ if (data.status === 'completed') {
40
+ completed.push(name);
41
+ if (data.validation_score) scores[name] = data.validation_score;
42
+ } else if (data.status === 'failed') {
43
+ failed.push(name);
44
+ }
45
+ }
46
+
47
+ const avgScore = Object.values(scores).length > 0
48
+ ? Math.round(Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length)
49
+ : null;
50
+
51
+ return {
52
+ time: now.toISOString().split('T')[1].split('.')[0],
53
+ phase: sessionState.project?.state || sessionState.mode || 'unknown',
54
+ current_agent: sessionState.current_agent || 'none',
55
+ tasks_completed: completed.length,
56
+ tasks_failed: failed.length,
57
+ agents_completed: completed,
58
+ agents_failed: failed,
59
+ avg_score: avgScore,
60
+ scores,
61
+ decisions: extras.decisions || [],
62
+ gotchas: extras.gotchas || [],
63
+ duration_min: extras.duration || null,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Append an entry to today's daily digest file.
69
+ *
70
+ * @param {string} projectDir - Project root
71
+ * @param {object} entry - Digest entry from buildDailyEntry()
72
+ * @returns {{ saved: boolean, path: string }}
73
+ */
74
+ export function appendDailyDigest(projectDir, entry) {
75
+ const dir = join(projectDir, DAILY_DIR);
76
+ mkdirSync(dir, { recursive: true });
77
+
78
+ const dateStr = new Date().toISOString().split('T')[0];
79
+ const filePath = join(dir, `${dateStr}.md`);
80
+
81
+ // Build markdown entry
82
+ const lines = [];
83
+ lines.push(`### ${entry.time} — ${entry.phase}`);
84
+ lines.push(`- **Agent**: ${entry.current_agent}`);
85
+ lines.push(`- **Completed**: ${entry.tasks_completed} | **Failed**: ${entry.tasks_failed}`);
86
+ if (entry.avg_score) lines.push(`- **Avg Score**: ${entry.avg_score}%`);
87
+ if (entry.duration_min) lines.push(`- **Duration**: ${entry.duration_min} min`);
88
+
89
+ if (entry.agents_completed.length > 0) {
90
+ lines.push(`- **Agents done**: ${entry.agents_completed.join(', ')}`);
91
+ }
92
+ if (entry.agents_failed.length > 0) {
93
+ lines.push(`- **Agents failed**: ${entry.agents_failed.join(', ')}`);
94
+ }
95
+ if (entry.decisions.length > 0) {
96
+ lines.push('- **Decisions**:');
97
+ for (const d of entry.decisions.slice(0, 5)) {
98
+ lines.push(` - ${d}`);
99
+ }
100
+ }
101
+ if (entry.gotchas.length > 0) {
102
+ lines.push('- **Gotchas**:');
103
+ for (const g of entry.gotchas.slice(0, 5)) {
104
+ lines.push(` - ${g}`);
105
+ }
106
+ }
107
+ lines.push('');
108
+
109
+ try {
110
+ let existing = '';
111
+ if (existsSync(filePath)) {
112
+ existing = readFileSync(filePath, 'utf-8');
113
+ } else {
114
+ // Create header for new daily file
115
+ existing = `# Daily Digest — ${dateStr}\n\n`;
116
+ }
117
+
118
+ writeFileSync(filePath, existing + lines.join('\n') + '\n', 'utf-8');
119
+ return { saved: true, path: filePath };
120
+ } catch (err) {
121
+ process.stderr.write(`[chati] daily-digest append: ${err.message}\n`);
122
+ return { saved: false, path: filePath };
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Load today's daily digest.
128
+ *
129
+ * @param {string} projectDir - Project root
130
+ * @returns {{ loaded: boolean, content: string|null, path: string }}
131
+ */
132
+ export function loadTodayDigest(projectDir) {
133
+ const dateStr = new Date().toISOString().split('T')[0];
134
+ const filePath = join(projectDir, DAILY_DIR, `${dateStr}.md`);
135
+
136
+ if (!existsSync(filePath)) {
137
+ return { loaded: false, content: null, path: filePath };
138
+ }
139
+
140
+ try {
141
+ const content = readFileSync(filePath, 'utf-8');
142
+ return { loaded: true, content, path: filePath };
143
+ } catch (err) {
144
+ process.stderr.write(`[chati] daily-digest load today: ${err.message}\n`);
145
+ return { loaded: false, content: null, path: filePath };
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Load a specific date's digest.
151
+ *
152
+ * @param {string} projectDir - Project root
153
+ * @param {string} dateStr - Date in YYYY-MM-DD format
154
+ * @returns {{ loaded: boolean, content: string|null, path: string }}
155
+ */
156
+ export function loadDigestByDate(projectDir, dateStr) {
157
+ const filePath = join(projectDir, DAILY_DIR, `${dateStr}.md`);
158
+
159
+ if (!existsSync(filePath)) {
160
+ return { loaded: false, content: null, path: filePath };
161
+ }
162
+
163
+ try {
164
+ const content = readFileSync(filePath, 'utf-8');
165
+ return { loaded: true, content, path: filePath };
166
+ } catch (err) {
167
+ process.stderr.write(`[chati] daily-digest load date: ${err.message}\n`);
168
+ return { loaded: false, content: null, path: filePath };
169
+ }
170
+ }