chati-dev 4.0.11 → 4.1.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 (116) hide show
  1. package/README.md +14 -9
  2. package/bin/chati.js +17 -1
  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-data-engineer.md +278 -0
  8. package/framework/agents/plan/architect-system.md +174 -0
  9. package/framework/agents/plan/architect.md +97 -301
  10. package/framework/agents/plan/detail.md +25 -0
  11. package/framework/agents/plan/tasks.md +29 -1
  12. package/framework/agents/plan/ux-brand-architect.md +215 -0
  13. package/framework/agents/plan/ux-component-engineer.md +289 -0
  14. package/framework/agents/plan/ux-researcher.md +166 -0
  15. package/framework/agents/plan/ux.md +126 -1008
  16. package/framework/agents/quality/qa-implementation.md +121 -22
  17. package/framework/agents/quality/qa-planning.md +18 -0
  18. package/framework/config.yaml +15 -4
  19. package/framework/constitution.md +9 -3
  20. package/framework/context/root.md +2 -2
  21. package/framework/data/entity-registry.yaml +59 -3
  22. package/framework/hooks/constitution-guard.js +67 -1
  23. package/framework/hooks/license-guard.js +4 -4
  24. package/framework/hooks/model-governance.js +2 -1
  25. package/framework/hooks/prism-engine.js +74 -6
  26. package/framework/hooks/read-protection.js +1 -1
  27. package/framework/hooks/session-digest.js +159 -7
  28. package/framework/hooks/settings.json +16 -4
  29. package/framework/hooks/style-guard.js +134 -0
  30. package/framework/hooks/undercover-guard.js +220 -0
  31. package/framework/i18n/en.yaml +2 -2
  32. package/framework/i18n/es.yaml +2 -2
  33. package/framework/i18n/fr.yaml +2 -2
  34. package/framework/i18n/pt.yaml +2 -2
  35. package/framework/intelligence/context-engine.md +21 -0
  36. package/framework/intelligence/hooks-performance.md +54 -0
  37. package/framework/intelligence/memory-layer.md +47 -0
  38. package/framework/migrations/v4.0-to-v4.1.yaml +165 -0
  39. package/framework/orchestrator/chati.md +327 -1067
  40. package/framework/schemas/session.schema.json +1 -1
  41. package/framework/templates/brandbook-html-tmpl.md +107 -0
  42. package/framework/templates/session-memory-tmpl.yaml +68 -0
  43. package/package.json +1 -1
  44. package/scripts/doctor/checks/agents.js +1 -1
  45. package/scripts/health-check.js +1 -1
  46. package/src/autonomy/build-state.js +1 -1
  47. package/src/autonomy/worktree-manager.js +13 -13
  48. package/src/config/agent-customizer.js +1 -1
  49. package/src/config/gemini-hooks-generator.js +149 -6
  50. package/src/config/ide-configs.js +1 -1
  51. package/src/context/bracket-tracker.js +25 -4
  52. package/src/context/engine.js +37 -7
  53. package/src/context/formatter.js +45 -1
  54. package/src/context/index.js +3 -3
  55. package/src/dashboard/data-reader.js +7 -7
  56. package/src/decision/engine.js +2 -2
  57. package/src/decision/registry-healer.js +1 -1
  58. package/src/decision/registry-updater.js +2 -2
  59. package/src/extensions/loader.js +1 -1
  60. package/src/gates/g1-planning-complete.js +1 -1
  61. package/src/gates/g2-qa-planning.js +1 -1
  62. package/src/gates/g3-implementation.js +4 -4
  63. package/src/gates/g4-qa-implementation.js +2 -2
  64. package/src/health/auto-fix.js +3 -3
  65. package/src/health/engine.js +2 -2
  66. package/src/installer/brownfield-upgrader.js +3 -3
  67. package/src/installer/manifest.js +1 -1
  68. package/src/installer/transaction.js +1 -1
  69. package/src/installer/validator.js +4 -4
  70. package/src/intelligence/decision-engine.js +1 -1
  71. package/src/intelligence/memory-manager.js +1 -1
  72. package/src/intelligence/registry-manager.js +2 -2
  73. package/src/intelligence/timeline.js +1 -1
  74. package/src/license/client.js +1 -1
  75. package/src/license/commands.js +2 -2
  76. package/src/memory/agent-memory.js +3 -3
  77. package/src/memory/daily-digest.js +170 -0
  78. package/src/memory/dream.js +254 -0
  79. package/src/memory/gotchas.js +2 -2
  80. package/src/memory/index.js +18 -0
  81. package/src/memory/magic-docs.js +98 -0
  82. package/src/memory/memory-extractor.js +163 -0
  83. package/src/memory/session-digest.js +144 -6
  84. package/src/merger/yaml-merger.js +1 -1
  85. package/src/orchestrator/cli.js +980 -0
  86. package/src/orchestrator/handoff-engine.js +31 -4
  87. package/src/orchestrator/index.js +9 -0
  88. package/src/orchestrator/session-manager.js +1 -1
  89. package/src/preview/detector.js +3 -3
  90. package/src/preview/launcher.js +2 -2
  91. package/src/quality/metrics-collector.js +1 -1
  92. package/src/quality/test-runner.js +2 -4
  93. package/src/scanning/density-scanner.js +51 -0
  94. package/src/scanning/env-scanner.js +97 -0
  95. package/src/scanning/index.js +7 -0
  96. package/src/scanning/leakage-scanner.js +54 -0
  97. package/src/scanning/placeholder-scanner.js +40 -0
  98. package/src/scanning/security-scanner.js +94 -0
  99. package/src/security/bash-security.js +335 -0
  100. package/src/security/index.js +9 -0
  101. package/src/telemetry/config.js +3 -3
  102. package/src/telemetry/sender.js +1 -1
  103. package/src/terminal/cli-registry.js +1 -1
  104. package/src/terminal/cost-tracker.js +1 -2
  105. package/src/terminal/prompt-builder.js +11 -27
  106. package/src/terminal/rate-limiter.js +1 -2
  107. package/src/terminal/run-agent.js +3 -3
  108. package/src/terminal/spawner.js +41 -4
  109. package/src/upgrade/checker.js +2 -2
  110. package/src/upgrade/migrator.js +1 -1
  111. package/src/utils/config-parser.js +1 -1
  112. package/src/utils/file-lock.js +3 -3
  113. package/src/utils/provider-limits.js +35 -4
  114. package/src/wizard/i18n.js +4 -4
  115. package/src/wizard/index.js +2 -1
  116. package/src/wizard/questions.js +4 -2
@@ -52,7 +52,19 @@ export function formatContext(options) {
52
52
  sections.push(formatKeywords(l5));
53
53
  }
54
54
 
55
- let body = sections.join('\n\n');
55
+ // Insert STATIC_BOUNDARY between static (L0+L1) and dynamic (L2-L5) sections.
56
+ // This enables prompt cache reuse for the stable prefix (Claude Code pattern).
57
+ const staticSections = sections.slice(0, l1 ? 2 : (l0 ? 1 : 0));
58
+ const dynamicSections = sections.slice(staticSections.length);
59
+
60
+ let body;
61
+ if (dynamicSections.length > 0) {
62
+ body = staticSections.join('\n\n') +
63
+ '\n\n <!-- STATIC_BOUNDARY -->\n\n' +
64
+ dynamicSections.join('\n\n');
65
+ } else {
66
+ body = staticSections.join('\n\n');
67
+ }
56
68
 
57
69
  // Truncate if over budget (rough estimate: 1 token ≈ 4 chars)
58
70
  const maxChars = tokenBudget * 4;
@@ -193,6 +205,38 @@ function truncateByPriority(sections, maxChars) {
193
205
  return result;
194
206
  }
195
207
 
208
+ /**
209
+ * Format only the static portion (L0 + L1) of the context.
210
+ * Useful for caching the stable prefix separately.
211
+ * @param {object} l0 - L0 Constitution result
212
+ * @param {object} [l1] - L1 Global result
213
+ * @returns {string} Static XML block
214
+ */
215
+ export function formatStaticBlock(l0, l1) {
216
+ const parts = [];
217
+ if (l0) parts.push(formatConstitution(l0));
218
+ if (l1) parts.push(formatGlobal(l1));
219
+ return parts.join('\n\n');
220
+ }
221
+
222
+ /**
223
+ * Format only the dynamic portion (L2-L5) of the context.
224
+ * Changes every turn based on agent, workflow, task, and keywords.
225
+ * @param {object} [l2] - L2 Agent result
226
+ * @param {object} [l3] - L3 Workflow result
227
+ * @param {object} [l4] - L4 Task result
228
+ * @param {object} [l5] - L5 Keywords result
229
+ * @returns {string} Dynamic XML block
230
+ */
231
+ export function formatDynamicBlock(l2, l3, l4, l5) {
232
+ const parts = [];
233
+ if (l2 && l2.agent) parts.push(formatAgent(l2));
234
+ if (l3 && l3.workflow) parts.push(formatWorkflow(l3));
235
+ if (l4 && l4.taskId) parts.push(formatTask(l4));
236
+ if (l5 && l5.matchCount > 0) parts.push(formatKeywords(l5));
237
+ return parts.join('\n\n');
238
+ }
239
+
196
240
  /** Escape XML special characters */
197
241
  function esc(str) {
198
242
  if (!str) return '';
@@ -1,4 +1,4 @@
1
- export { runPrism, getPrismInfo } from './engine.js';
2
- export { calculateBracket, estimateRemaining, isLayerActive, getBracketDefinitions } from './bracket-tracker.js';
3
- export { formatContext } from './formatter.js';
1
+ export { runPrism, getPrismInfo, clearStaticCache } from './engine.js';
2
+ export { calculateBracket, estimateRemaining, estimateRemainingFromTokens, isLayerActive, getBracketDefinitions } from './bracket-tracker.js';
3
+ export { formatContext, formatStaticBlock, formatDynamicBlock } from './formatter.js';
4
4
  export { loadDomainFile, loadAgentDomains, loadWorkflowDomains, extractRules } from './domain-loader.js';
@@ -25,7 +25,7 @@ export async function readDashboardData(targetDir) {
25
25
  if (existsSync(sessionPath)) {
26
26
  try {
27
27
  data.session = yaml.load(readFileSync(sessionPath, 'utf-8'));
28
- } catch {
28
+ } catch { /* expected: file may not exist */
29
29
  data.session = null;
30
30
  }
31
31
  }
@@ -35,7 +35,7 @@ export async function readDashboardData(targetDir) {
35
35
  if (existsSync(configPath)) {
36
36
  try {
37
37
  data.config = yaml.load(readFileSync(configPath, 'utf-8'));
38
- } catch {
38
+ } catch { /* expected: file may not exist */
39
39
  data.config = null;
40
40
  }
41
41
  }
@@ -64,7 +64,7 @@ export async function readDashboardData(targetDir) {
64
64
  try {
65
65
  const files = readdirSync(tasksDir).filter(f => f.endsWith('.yaml') || f.endsWith('.md'));
66
66
  data.taskProgress = { totalFiles: files.length };
67
- } catch {
67
+ } catch { /* expected: directory may not exist */
68
68
  // Ignore
69
69
  }
70
70
  }
@@ -75,7 +75,7 @@ export async function readDashboardData(targetDir) {
75
75
  try {
76
76
  const gotchasData = yaml.load(readFileSync(gotchasPath, 'utf-8'));
77
77
  data.gotchas = gotchasData?.gotchas || [];
78
- } catch {
78
+ } catch { /* expected: file may not exist */
79
79
  // Ignore
80
80
  }
81
81
  }
@@ -102,21 +102,21 @@ export async function readDashboardData(targetDir) {
102
102
  try {
103
103
  const { getMemoryStats } = await import('../intelligence/memory-manager.js');
104
104
  data.memoryStats = getMemoryStats(targetDir);
105
- } catch {
105
+ } catch { /* expected: operation may fail gracefully */
106
106
  // Intelligence module not available
107
107
  }
108
108
 
109
109
  try {
110
110
  const { getContextStatus } = await import('../intelligence/context-status.js');
111
111
  data.contextStatus = getContextStatus(targetDir);
112
- } catch {
112
+ } catch { /* expected: operation may fail gracefully */
113
113
  // Intelligence module not available
114
114
  }
115
115
 
116
116
  try {
117
117
  const { getRegistryStats } = await import('../intelligence/registry-manager.js');
118
118
  data.registryStats = getRegistryStats(targetDir);
119
- } catch {
119
+ } catch { /* expected: operation may fail gracefully */
120
120
  // Intelligence module not available
121
121
  }
122
122
 
@@ -168,7 +168,7 @@ export function getDecisionHistory(projectDir) {
168
168
  const content = readFileSync(historyPath, 'utf8');
169
169
  const data = JSON.parse(content);
170
170
  return data.decisions || [];
171
- } catch {
171
+ } catch { /* expected: file may not exist */
172
172
  return [];
173
173
  }
174
174
  }
@@ -193,7 +193,7 @@ export function recordDecision(projectDir, decision) {
193
193
  try {
194
194
  const content = readFileSync(historyPath, 'utf8');
195
195
  history = JSON.parse(content);
196
- } catch {
196
+ } catch { /* expected: file may not exist */
197
197
  // Keep empty history if parse fails
198
198
  }
199
199
  }
@@ -439,7 +439,7 @@ export function applyFixes(projectDir, fixes) {
439
439
  applied++;
440
440
  break;
441
441
  }
442
- } catch {
442
+ } catch { /* expected: operation may fail gracefully */
443
443
  // Skip failed fixes
444
444
  }
445
445
  });
@@ -290,7 +290,7 @@ export function generateEntityMeta(projectDir, filePath) {
290
290
  .slice(0, 5)
291
291
  .map(([word]) => word);
292
292
  meta.keywords.push(...topWords);
293
- } catch {
293
+ } catch { /* expected: operation may fail gracefully */
294
294
  // Ignore read errors
295
295
  }
296
296
  }
@@ -322,7 +322,7 @@ function scanDirectory(dirPath) {
322
322
  files.push(fullPath);
323
323
  }
324
324
  });
325
- } catch {
325
+ } catch { /* expected: operation may fail gracefully */
326
326
  // Ignore read errors
327
327
  }
328
328
 
@@ -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
  }
@@ -8,7 +8,7 @@ import { validateSchema, CONFIG_SCHEMA } from '../utils/schema-validator.js';
8
8
 
9
9
  /**
10
10
  * Validate chati.dev installation
11
- * Checks all 13 agents, constitution, session, schemas, etc.
11
+ * Checks all primary agents, constitution, session, schemas, etc.
12
12
  */
13
13
  export async function validateInstallation(targetDir) {
14
14
  const results = {
@@ -28,7 +28,7 @@ export async function validateInstallation(targetDir) {
28
28
  passed: 0,
29
29
  };
30
30
 
31
- // Check all 13 agents (orchestrator + 12 specialized)
31
+ // Check all primary agents (orchestrator + 12 specialized, sub-agents validated by managers)
32
32
  const agentFiles = [
33
33
  'orchestrator/chati.md',
34
34
  'agents/discover/greenfield-wu.md',
@@ -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
  }
@@ -142,7 +142,7 @@ export async function deactivateLicense(_projectDir) {
142
142
  body: JSON.stringify({ key, machine_id: machineId }),
143
143
  signal: AbortSignal.timeout(5000),
144
144
  });
145
- } catch {
145
+ } catch { /* expected: network may be unavailable */
146
146
  // Fail silently — local deactivation still proceeds
147
147
  }
148
148
  }
@@ -79,8 +79,8 @@ export async function runLicenseStatus(projectDir) {
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
+ }