auroq-os 1.0.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 (90) hide show
  1. package/.auroq-core/constitution.md +153 -0
  2. package/.auroq-core/core/synapse/context/context-builder.js +34 -0
  3. package/.auroq-core/core/synapse/context/context-tracker.js +198 -0
  4. package/.auroq-core/core/synapse/diagnostics/collectors/consistency-collector.js +168 -0
  5. package/.auroq-core/core/synapse/diagnostics/collectors/hook-collector.js +129 -0
  6. package/.auroq-core/core/synapse/diagnostics/collectors/manifest-collector.js +82 -0
  7. package/.auroq-core/core/synapse/diagnostics/collectors/output-analyzer.js +134 -0
  8. package/.auroq-core/core/synapse/diagnostics/collectors/pipeline-collector.js +75 -0
  9. package/.auroq-core/core/synapse/diagnostics/collectors/quality-collector.js +252 -0
  10. package/.auroq-core/core/synapse/diagnostics/collectors/relevance-matrix.js +174 -0
  11. package/.auroq-core/core/synapse/diagnostics/collectors/safe-read-json.js +31 -0
  12. package/.auroq-core/core/synapse/diagnostics/collectors/session-collector.js +102 -0
  13. package/.auroq-core/core/synapse/diagnostics/collectors/timing-collector.js +126 -0
  14. package/.auroq-core/core/synapse/diagnostics/collectors/uap-collector.js +83 -0
  15. package/.auroq-core/core/synapse/diagnostics/report-formatter.js +484 -0
  16. package/.auroq-core/core/synapse/diagnostics/synapse-diagnostics.js +95 -0
  17. package/.auroq-core/core/synapse/domain/domain-loader.js +322 -0
  18. package/.auroq-core/core/synapse/engine.js +400 -0
  19. package/.auroq-core/core/synapse/layers/l0-constitution.js +80 -0
  20. package/.auroq-core/core/synapse/layers/l1-global.js +102 -0
  21. package/.auroq-core/core/synapse/layers/l2-agent.js +94 -0
  22. package/.auroq-core/core/synapse/layers/l3-workflow.js +94 -0
  23. package/.auroq-core/core/synapse/layers/l4-task.js +83 -0
  24. package/.auroq-core/core/synapse/layers/l5-squad.js +244 -0
  25. package/.auroq-core/core/synapse/layers/l6-keyword.js +154 -0
  26. package/.auroq-core/core/synapse/layers/l7-star-command.js +169 -0
  27. package/.auroq-core/core/synapse/layers/layer-processor.js +82 -0
  28. package/.auroq-core/core/synapse/memory/memory-bridge.js +220 -0
  29. package/.auroq-core/core/synapse/memory/synapse-memory-provider.js +201 -0
  30. package/.auroq-core/core/synapse/output/formatter.js +561 -0
  31. package/.auroq-core/core/synapse/runtime/hook-runtime.js +98 -0
  32. package/.auroq-core/core/synapse/scripts/generate-constitution.js +204 -0
  33. package/.auroq-core/core/synapse/session/session-manager.js +404 -0
  34. package/.auroq-core/core/synapse/utils/atomic-write.js +79 -0
  35. package/.auroq-core/core/synapse/utils/paths.js +57 -0
  36. package/.auroq-core/core/synapse/utils/tokens.js +25 -0
  37. package/.auroq-core/core-config.yaml +80 -0
  38. package/.auroq-core/development/sistema-gestao-projetos.md +193 -0
  39. package/.auroq-core/development/sistema-memoria.md +157 -0
  40. package/.auroq-core/dna-operacional.md +134 -0
  41. package/.claude/CLAUDE.md +399 -0
  42. package/.claude/commands/AuroqOS/agents/forge.md +374 -0
  43. package/.claude/commands/AuroqOS/agents/ops.md +277 -0
  44. package/.claude/commands/companion.md +7 -0
  45. package/.claude/hooks/precompact-session-digest.cjs +46 -0
  46. package/.claude/hooks/synapse-engine.cjs +99 -0
  47. package/.claude/rules/agent-authority.md +79 -0
  48. package/.claude/rules/agent-handoff.md +97 -0
  49. package/.claude/rules/commit-inteligente.md +86 -0
  50. package/.claude/rules/dna-operacional.md +53 -0
  51. package/.claude/rules/evolucao-incremental.md +43 -0
  52. package/.claude/rules/mcp-usage.md +38 -0
  53. package/.claude/rules/memoria-inteligente.md +65 -0
  54. package/.claude/rules/project-tracker.md +49 -0
  55. package/.claude/rules/tool-response-filtering.md +57 -0
  56. package/.claude/settings.json +1 -0
  57. package/.claude/settings.local.json +33 -0
  58. package/.env.example +22 -0
  59. package/.gitignore +23 -0
  60. package/.synapse/constitution +21 -0
  61. package/.synapse/context +8 -0
  62. package/.synapse/global +10 -0
  63. package/.synapse/manifest +35 -0
  64. package/README.md +67 -0
  65. package/agents/companion/agents/companion.md +306 -0
  66. package/agents/companion/data/contexto-dinamico.md +23 -0
  67. package/agents/companion/data/demandas-backlog.md +19 -0
  68. package/agents/companion/data/log-decisoes.md +11 -0
  69. package/agents/companion/data/padroes-observados.md +10 -0
  70. package/agents/companion/knowledge/companion-foundation.md +49 -0
  71. package/agents/companion/knowledge/expert-essencial.md +30 -0
  72. package/agents/companion/knowledge/modus-operandi.md +161 -0
  73. package/agents/companion/tasks/new-project.md +111 -0
  74. package/agents/companion/tasks/save-memory.md +73 -0
  75. package/agents/companion/tasks/start.md +136 -0
  76. package/agents/companion/tasks/weekly-review.md +82 -0
  77. package/bin/auroq-os.js +346 -0
  78. package/business/campanhas/_template/BRIEFING.md +45 -0
  79. package/business/campanhas/_template/CHECKLIST.md +29 -0
  80. package/business/campanhas/_template/DIARIO.md +15 -0
  81. package/business/campanhas/_template/RETROVISOR.md +34 -0
  82. package/business/cockpit.md +72 -0
  83. package/business/templates/tracker-template.md +71 -0
  84. package/docs/knowledge/expert-business/posicionamento.md +27 -0
  85. package/docs/knowledge/expert-business/publico-alvo.md +39 -0
  86. package/docs/knowledge/expert-mind/historia.md +27 -0
  87. package/docs/knowledge/expert-mind/identidade.md +23 -0
  88. package/docs/knowledge/expert-mind/tom-de-voz.md +30 -0
  89. package/docs/knowledge/expert-mind/valores.md +32 -0
  90. package/package.json +26 -0
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Constitution Generator
3
+ *
4
+ * Reads .auroq-core/constitution.md and generates .synapse/constitution
5
+ * in KEY=VALUE format for the SYNAPSE Context Engine L0 layer.
6
+ *
7
+ * Usage: node .auroq-core/core/synapse/scripts/generate-constitution.js
8
+ *
9
+ * @module core/synapse/scripts/generate-constitution
10
+ * @version 1.0.0
11
+ * @created Story SYN-8 - Domain Content Files
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ /**
18
+ * Roman numeral to Arabic number map (I-X)
19
+ */
20
+ const ROMAN_TO_ARABIC = {
21
+ 'I': 1, 'II': 2, 'III': 3, 'IV': 4, 'V': 5,
22
+ 'VI': 6, 'VII': 7, 'VIII': 8, 'IX': 9, 'X': 10,
23
+ };
24
+
25
+ /**
26
+ * Clean markdown formatting from text
27
+ * Removes backticks and trims whitespace
28
+ *
29
+ * @param {string} text - Text with possible markdown formatting
30
+ * @returns {string} Cleaned text
31
+ */
32
+ function cleanText(text) {
33
+ return text.replace(/`/g, '').trim();
34
+ }
35
+
36
+ /**
37
+ * Parse constitution.md and extract articles with their rules
38
+ *
39
+ * @param {string} content - Raw markdown content of constitution.md
40
+ * @returns {Array<{number: number, roman: string, title: string, severity: string, rules: string[]}>}
41
+ */
42
+ function parseConstitution(content) {
43
+ if (!content || typeof content !== 'string') {
44
+ return [];
45
+ }
46
+
47
+ const articles = [];
48
+
49
+ // Match article headers: ### I. Title (SEVERITY)
50
+ const articleRegex = /^### ([IVXLC]+)\.\s+(.+?)\s*\(([^)]+)\)\s*$/gm;
51
+
52
+ let match;
53
+ const articlePositions = [];
54
+
55
+ while ((match = articleRegex.exec(content)) !== null) {
56
+ articlePositions.push({
57
+ roman: match[1],
58
+ title: match[2].trim(),
59
+ severity: match[3].trim(),
60
+ startIndex: match.index,
61
+ });
62
+ }
63
+
64
+ for (let i = 0; i < articlePositions.length; i++) {
65
+ const start = articlePositions[i].startIndex;
66
+ const end = i + 1 < articlePositions.length
67
+ ? articlePositions[i + 1].startIndex
68
+ : content.indexOf('## Governance', start) !== -1
69
+ ? content.indexOf('## Governance', start)
70
+ : content.length;
71
+
72
+ const articleContent = content.substring(start, end);
73
+ const rules = extractRules(articleContent);
74
+
75
+ const num = ROMAN_TO_ARABIC[articlePositions[i].roman];
76
+ if (!num) {
77
+ continue;
78
+ }
79
+
80
+ articles.push({
81
+ number: num,
82
+ roman: articlePositions[i].roman,
83
+ title: articlePositions[i].title,
84
+ severity: articlePositions[i].severity,
85
+ rules,
86
+ });
87
+ }
88
+
89
+ return articles;
90
+ }
91
+
92
+ /**
93
+ * Extract rules from article content
94
+ * Matches bullet points starting with MUST:, MUST NOT:, SHOULD:, SHOULD NOT:, EXCEPTION:
95
+ *
96
+ * @param {string} articleContent - Markdown content of a single article
97
+ * @returns {string[]} Array of rule strings
98
+ */
99
+ function extractRules(articleContent) {
100
+ const rules = [];
101
+ const lines = articleContent.split('\n');
102
+
103
+ for (const line of lines) {
104
+ const trimmed = line.trim();
105
+ const ruleMatch = trimmed.match(/^-\s+(MUST(?:\s+NOT)?|SHOULD(?:\s+NOT)?|EXCEPTION):\s+(.+)$/);
106
+ if (ruleMatch) {
107
+ rules.push(`${ruleMatch[1]}: ${cleanText(ruleMatch[2])}`);
108
+ }
109
+ }
110
+
111
+ return rules;
112
+ }
113
+
114
+ /**
115
+ * Generate KEY=VALUE constitution content from parsed articles
116
+ *
117
+ * @param {Array<{number: number, roman: string, title: string, severity: string, rules: string[]}>} articles
118
+ * @returns {string} KEY=VALUE formatted content
119
+ */
120
+ function generateConstitution(articles) {
121
+ const lines = [
122
+ '# SYNAPSE Constitution Domain (L0)',
123
+ '# Auto-generated from .auroq-core/constitution.md',
124
+ '# DO NOT EDIT MANUALLY — re-run generate-constitution.js',
125
+ '',
126
+ ];
127
+
128
+ for (const article of articles) {
129
+ lines.push(`# Article ${article.roman}: ${article.title} (${article.severity})`);
130
+
131
+ // Rule 0: title + severity summary
132
+ lines.push(`CONSTITUTION_RULE_ART${article.number}_0=${article.title} (${article.severity})`);
133
+
134
+ // Subsequent rules from bullet points
135
+ for (let i = 0; i < article.rules.length; i++) {
136
+ lines.push(`CONSTITUTION_RULE_ART${article.number}_${i + 1}=${article.rules[i]}`);
137
+ }
138
+
139
+ lines.push('');
140
+ }
141
+
142
+ return lines.join('\n');
143
+ }
144
+
145
+ /**
146
+ * Main entry point: read constitution.md, generate .synapse/constitution
147
+ *
148
+ * @param {object} [options] - Override paths for testing
149
+ * @param {string} [options.projectRoot] - Project root directory
150
+ * @param {string} [options.constitutionPath] - Path to constitution.md
151
+ * @param {string} [options.outputPath] - Path to output file
152
+ * @returns {{success: boolean, articles?: number, rules?: number, outputPath?: string, error?: string}}
153
+ */
154
+ function main(options = {}) {
155
+ const projectRoot = options.projectRoot || path.resolve(__dirname, '..', '..', '..', '..');
156
+ const constitutionPath = options.constitutionPath || path.join(projectRoot, '.auroq-core', 'constitution.md');
157
+ const outputPath = options.outputPath || path.join(projectRoot, '.synapse', 'constitution');
158
+
159
+ // Read source
160
+ let content;
161
+ try {
162
+ content = fs.readFileSync(constitutionPath, 'utf8');
163
+ } catch (error) {
164
+ if (error.code === 'ENOENT') {
165
+ console.error(`Constitution not found: ${constitutionPath}`);
166
+ process.exitCode = 1;
167
+ return { success: false, error: 'Constitution file not found' };
168
+ }
169
+ throw error;
170
+ }
171
+
172
+ // Parse articles
173
+ const articles = parseConstitution(content);
174
+
175
+ if (articles.length === 0) {
176
+ console.error('No articles found in constitution.md');
177
+ process.exitCode = 1;
178
+ return { success: false, error: 'No articles found' };
179
+ }
180
+
181
+ // Generate output
182
+ const output = generateConstitution(articles);
183
+
184
+ // Ensure output directory exists
185
+ const outputDir = path.dirname(outputPath);
186
+ if (!fs.existsSync(outputDir)) {
187
+ fs.mkdirSync(outputDir, { recursive: true });
188
+ }
189
+
190
+ // Write output (idempotent — overwrites cleanly)
191
+ fs.writeFileSync(outputPath, output, 'utf8');
192
+
193
+ const totalRules = articles.reduce((sum, a) => sum + a.rules.length + 1, 0);
194
+ console.log(`Constitution generated: ${articles.length} articles, ${totalRules} rules`);
195
+
196
+ return { success: true, articles: articles.length, rules: totalRules, outputPath };
197
+ }
198
+
199
+ // Run if called directly
200
+ if (require.main === module) {
201
+ main();
202
+ }
203
+
204
+ module.exports = { parseConstitution, extractRules, generateConstitution, cleanText, main, ROMAN_TO_ARABIC };
@@ -0,0 +1,404 @@
1
+ /**
2
+ * SYNAPSE Session Manager
3
+ *
4
+ * Manages session state (active agent, workflow, task, squad) persisted as JSON.
5
+ * Sessions are stored in `.synapse/sessions/{uuid}.json` using schema v2.0.
6
+ *
7
+ * Features:
8
+ * - CRUD operations for sessions
9
+ * - Stale session cleanup (>24h inactive)
10
+ * - Auto-title generation from first meaningful prompt
11
+ * - Gitignore creation for sessions/ and cache/
12
+ *
13
+ * @module core/synapse/session/session-manager
14
+ * @version 1.0.0
15
+ * @created Story SYN-2 - Session Manager
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { atomicWriteSync } = require('../utils/atomic-write');
21
+
22
+ const SCHEMA_VERSION = '2.0';
23
+ const DEFAULT_MAX_AGE_HOURS = 24;
24
+ const MAX_TITLE_LENGTH = 50;
25
+
26
+ /**
27
+ * Build the default session object conforming to schema v2.0
28
+ *
29
+ * @param {string} sessionId - UUID from Claude Code
30
+ * @param {string} cwd - Working directory
31
+ * @returns {object} Session object with schema v2.0 defaults
32
+ */
33
+ function buildDefaultSession(sessionId, cwd) {
34
+ const now = new Date().toISOString();
35
+ const label = path.basename(cwd);
36
+
37
+ return {
38
+ uuid: sessionId,
39
+ schema_version: SCHEMA_VERSION,
40
+ started: now,
41
+ last_activity: now,
42
+ cwd,
43
+ label,
44
+ title: null,
45
+ prompt_count: 0,
46
+ active_agent: { id: null, activated_at: null, activation_quality: null },
47
+ active_workflow: null,
48
+ active_squad: null,
49
+ active_task: null,
50
+ context: { last_bracket: 'FRESH', last_tokens_used: 0, last_context_percent: 100 },
51
+ overrides: {},
52
+ history: { star_commands_used: [], domains_loaded_last: [], agents_activated: [] },
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Resolve sessions directory path from a base .synapse/ path
58
+ *
59
+ * @param {string} sessionsDir - Path to sessions directory
60
+ * @returns {string} Resolved sessions directory path
61
+ */
62
+ function resolveSessionFile(sessionId, sessionsDir) {
63
+ // Sanitize sessionId to prevent path traversal
64
+ if (typeof sessionId !== 'string' || sessionId.includes('..') || sessionId.includes('/') || sessionId.includes('\\')) {
65
+ throw new Error('[synapse:session] Invalid sessionId: contains path separators or traversal');
66
+ }
67
+
68
+ const filePath = path.join(sessionsDir, `${sessionId}.json`);
69
+ const resolved = path.resolve(filePath);
70
+ const resolvedDir = path.resolve(sessionsDir);
71
+
72
+ if (!resolved.startsWith(resolvedDir + path.sep) && resolved !== resolvedDir) {
73
+ throw new Error('[synapse:session] Invalid sessionId: resolved path escapes sessions directory');
74
+ }
75
+
76
+ return filePath;
77
+ }
78
+
79
+ /**
80
+ * Ensure a directory exists, creating it recursively if needed
81
+ *
82
+ * @param {string} dirPath - Directory path to ensure
83
+ */
84
+ function ensureDir(dirPath) {
85
+ if (!fs.existsSync(dirPath)) {
86
+ fs.mkdirSync(dirPath, { recursive: true });
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Ensure .synapse/.gitignore exists with required entries
92
+ *
93
+ * @param {string} synapsePath - Path to .synapse/ directory
94
+ */
95
+ function ensureGitignore(synapsePath) {
96
+ const gitignorePath = path.join(synapsePath, '.gitignore');
97
+
98
+ if (fs.existsSync(gitignorePath)) {
99
+ return;
100
+ }
101
+
102
+ const content = [
103
+ '# SYNAPSE runtime data (auto-generated)',
104
+ 'sessions/',
105
+ 'cache/',
106
+ '',
107
+ ].join('\n');
108
+
109
+ ensureDir(synapsePath);
110
+ fs.writeFileSync(gitignorePath, content, 'utf8');
111
+ }
112
+
113
+ /**
114
+ * Create a new session
115
+ *
116
+ * Creates the session JSON file with schema v2.0 defaults.
117
+ * Auto-creates the sessions/ directory and .gitignore if they don't exist.
118
+ *
119
+ * @param {string} sessionId - UUID from Claude Code
120
+ * @param {string} cwd - Working directory
121
+ * @param {string} [sessionsDir] - Sessions directory (default: cwd/.synapse/sessions)
122
+ * @returns {object} Created session object
123
+ */
124
+ function createSession(sessionId, cwd, sessionsDir) {
125
+ const dir = sessionsDir || path.join(cwd, '.synapse', 'sessions');
126
+ ensureDir(dir);
127
+
128
+ // Ensure .gitignore exists in .synapse/
129
+ const synapsePath = path.dirname(dir);
130
+ ensureGitignore(synapsePath);
131
+
132
+ const session = buildDefaultSession(sessionId, cwd);
133
+ const filePath = resolveSessionFile(sessionId, dir);
134
+
135
+ try {
136
+ atomicWriteSync(filePath, JSON.stringify(session, null, 2));
137
+ } catch (error) {
138
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
139
+ console.error(`[synapse:session] Error: Permission denied creating session ${sessionId}`);
140
+ return null;
141
+ }
142
+ throw error;
143
+ }
144
+
145
+ return session;
146
+ }
147
+
148
+ /**
149
+ * Load an existing session
150
+ *
151
+ * @param {string} sessionId - Session UUID
152
+ * @param {string} sessionsDir - Path to sessions directory
153
+ * @returns {object|null} Session object, or null if not found or corrupted
154
+ */
155
+ function loadSession(sessionId, sessionsDir) {
156
+ const filePath = resolveSessionFile(sessionId, sessionsDir);
157
+
158
+ try {
159
+ if (!fs.existsSync(filePath)) {
160
+ return null;
161
+ }
162
+
163
+ const raw = fs.readFileSync(filePath, 'utf8');
164
+ const session = JSON.parse(raw);
165
+
166
+ if (session.schema_version !== SCHEMA_VERSION) {
167
+ console.warn(
168
+ `[synapse:session] Warning: Session ${sessionId} has schema_version "${session.schema_version}", expected "${SCHEMA_VERSION}"`,
169
+ );
170
+ return null;
171
+ }
172
+
173
+ return session;
174
+ } catch (error) {
175
+ if (error instanceof SyntaxError) {
176
+ console.warn(`[synapse:session] Warning: Corrupted JSON for session ${sessionId}`);
177
+ return null;
178
+ }
179
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
180
+ console.error(`[synapse:session] Error: Permission denied reading session ${sessionId}`);
181
+ return null;
182
+ }
183
+ throw error;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Update an existing session with partial merge
189
+ *
190
+ * Deep-merges the updates into the existing session.
191
+ * Increments prompt_count and updates last_activity on every call.
192
+ *
193
+ * @param {string} sessionId - Session UUID
194
+ * @param {string} sessionsDir - Path to sessions directory
195
+ * @param {object} updates - Partial updates to merge
196
+ * @returns {object|null} Updated session, or null if session not found
197
+ */
198
+ function updateSession(sessionId, sessionsDir, updates) {
199
+ const session = loadSession(sessionId, sessionsDir);
200
+
201
+ if (!session) {
202
+ return null;
203
+ }
204
+
205
+ // Shallow merge top-level fields
206
+ for (const [key, value] of Object.entries(updates)) {
207
+ if (key === 'history' && typeof value === 'object' && value !== null) {
208
+ // Merge history arrays by appending unique values
209
+ session.history = mergeHistory(session.history, value);
210
+ } else if (key === 'overrides' && typeof value === 'object' && value !== null) {
211
+ // Merge overrides
212
+ Object.assign(session.overrides, value);
213
+ } else if (key === 'context' && typeof value === 'object' && value !== null) {
214
+ // Merge context
215
+ Object.assign(session.context, value);
216
+ } else if (key === 'active_agent' && typeof value === 'object' && value !== null) {
217
+ // Replace active_agent entirely
218
+ session.active_agent = value;
219
+ } else {
220
+ session[key] = value;
221
+ }
222
+ }
223
+
224
+ // Always increment prompt_count and update last_activity
225
+ session.prompt_count += 1;
226
+ session.last_activity = new Date().toISOString();
227
+
228
+ const filePath = resolveSessionFile(sessionId, sessionsDir);
229
+
230
+ try {
231
+ atomicWriteSync(filePath, JSON.stringify(session, null, 2));
232
+ } catch (error) {
233
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
234
+ console.error(`[synapse:session] Error: Permission denied writing session ${sessionId}`);
235
+ return null;
236
+ }
237
+ throw error;
238
+ }
239
+
240
+ return session;
241
+ }
242
+
243
+ /**
244
+ * Merge history objects by appending unique values to arrays
245
+ *
246
+ * @param {object} existing - Existing history
247
+ * @param {object} incoming - Incoming history updates
248
+ * @returns {object} Merged history
249
+ */
250
+ function mergeHistory(existing, incoming) {
251
+ const merged = { ...existing };
252
+
253
+ for (const [key, value] of Object.entries(incoming)) {
254
+ if (Array.isArray(value) && Array.isArray(merged[key])) {
255
+ const combined = [...merged[key]];
256
+ for (const item of value) {
257
+ if (!combined.includes(item)) {
258
+ combined.push(item);
259
+ }
260
+ }
261
+ merged[key] = combined;
262
+ } else {
263
+ merged[key] = value;
264
+ }
265
+ }
266
+
267
+ return merged;
268
+ }
269
+
270
+ /**
271
+ * Delete a session
272
+ *
273
+ * @param {string} sessionId - Session UUID
274
+ * @param {string} sessionsDir - Path to sessions directory
275
+ * @returns {boolean} True if deleted, false if not found
276
+ */
277
+ function deleteSession(sessionId, sessionsDir) {
278
+ const filePath = resolveSessionFile(sessionId, sessionsDir);
279
+
280
+ try {
281
+ if (!fs.existsSync(filePath)) {
282
+ return false;
283
+ }
284
+
285
+ fs.unlinkSync(filePath);
286
+ return true;
287
+ } catch (error) {
288
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
289
+ console.error(`[synapse:session] Error: Permission denied deleting session ${sessionId}`);
290
+ return false;
291
+ }
292
+ throw error;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Clean stale sessions (inactive for more than maxAgeHours)
298
+ *
299
+ * Reads each session file individually to avoid loading all into memory.
300
+ * Does not fail if sessions directory doesn't exist.
301
+ *
302
+ * @param {string} sessionsDir - Path to sessions directory
303
+ * @param {number} [maxAgeHours=24] - Maximum inactivity threshold in hours
304
+ * @returns {number} Count of removed sessions
305
+ */
306
+ function cleanStaleSessions(sessionsDir, maxAgeHours = DEFAULT_MAX_AGE_HOURS) {
307
+ ensureDir(sessionsDir);
308
+
309
+ let removedCount = 0;
310
+ const cutoffMs = maxAgeHours * 60 * 60 * 1000;
311
+ const now = Date.now();
312
+
313
+ let files;
314
+ try {
315
+ files = fs.readdirSync(sessionsDir);
316
+ } catch (_error) {
317
+ return 0;
318
+ }
319
+
320
+ for (const file of files) {
321
+ if (!file.endsWith('.json')) {
322
+ continue;
323
+ }
324
+
325
+ const filePath = path.join(sessionsDir, file);
326
+
327
+ try {
328
+ const raw = fs.readFileSync(filePath, 'utf8');
329
+ const session = JSON.parse(raw);
330
+
331
+ if (session.last_activity) {
332
+ const lastActivity = new Date(session.last_activity).getTime();
333
+ const age = now - lastActivity;
334
+
335
+ if (age > cutoffMs) {
336
+ fs.unlinkSync(filePath);
337
+ removedCount++;
338
+ }
339
+ }
340
+ } catch (_error) {
341
+ // Skip corrupted files during cleanup
342
+ continue;
343
+ }
344
+ }
345
+
346
+ return removedCount;
347
+ }
348
+
349
+ /**
350
+ * Generate a title from a user prompt
351
+ *
352
+ * Extracts a meaningful title (max 50 chars) from the first significant prompt.
353
+ * Ignores *commands and single-word prompts.
354
+ *
355
+ * @param {string} prompt - User prompt text
356
+ * @returns {string|null} Generated title, or null if prompt is not title-worthy
357
+ */
358
+ function generateTitle(prompt) {
359
+ if (!prompt || typeof prompt !== 'string') {
360
+ return null;
361
+ }
362
+
363
+ const trimmed = prompt.trim();
364
+
365
+ // Ignore *commands
366
+ if (trimmed.startsWith('*')) {
367
+ return null;
368
+ }
369
+
370
+ // Ignore single words
371
+ if (!trimmed.includes(' ')) {
372
+ return null;
373
+ }
374
+
375
+ // Ignore very short prompts (less than 3 chars)
376
+ if (trimmed.length < 3) {
377
+ return null;
378
+ }
379
+
380
+ // Truncate to max length, respecting word boundaries
381
+ if (trimmed.length <= MAX_TITLE_LENGTH) {
382
+ return trimmed;
383
+ }
384
+
385
+ const truncated = trimmed.substring(0, MAX_TITLE_LENGTH);
386
+ const lastSpace = truncated.lastIndexOf(' ');
387
+
388
+ if (lastSpace > 0) {
389
+ return truncated.substring(0, lastSpace);
390
+ }
391
+
392
+ return truncated;
393
+ }
394
+
395
+ module.exports = {
396
+ createSession,
397
+ loadSession,
398
+ updateSession,
399
+ deleteSession,
400
+ cleanStaleSessions,
401
+ generateTitle,
402
+ ensureGitignore,
403
+ SCHEMA_VERSION,
404
+ };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Atomic Write Utility
3
+ *
4
+ * Writes files atomically using write-to-tmp + rename pattern.
5
+ * Prevents file corruption on unexpected exit (crash, kill, power loss).
6
+ *
7
+ * Pattern:
8
+ * 1. Write data to {filePath}.tmp.{pid}
9
+ * 2. On Windows: unlink target if exists (rename won't overwrite)
10
+ * 3. Rename tmp → target (atomic on POSIX, near-atomic on Windows)
11
+ * 4. On failure: clean up tmp file
12
+ *
13
+ * @module core/synapse/utils/atomic-write
14
+ * @version 1.0.0
15
+ * @created Story NOG-12 - State Persistence Hardening
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ const IS_WINDOWS = process.platform === 'win32';
24
+
25
+ /**
26
+ * Write data to a file atomically.
27
+ *
28
+ * Writes to a temporary file first, then renames to the target path.
29
+ * If the process crashes between write and rename, the original file
30
+ * remains intact and the orphaned .tmp file is harmless.
31
+ *
32
+ * @param {string} filePath - Target file path
33
+ * @param {string} data - Data to write
34
+ * @param {string} [encoding='utf8'] - File encoding
35
+ * @throws {Error} If write or rename fails (original file preserved)
36
+ */
37
+ function atomicWriteSync(filePath, data, encoding = 'utf8') {
38
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
39
+
40
+ try {
41
+ // Ensure parent directory exists
42
+ const dir = path.dirname(filePath);
43
+ if (!fs.existsSync(dir)) {
44
+ fs.mkdirSync(dir, { recursive: true });
45
+ }
46
+
47
+ // Step 1: Write to temporary file
48
+ fs.writeFileSync(tmpPath, data, encoding);
49
+
50
+ // Step 2: On Windows, unlink target first (rename won't overwrite)
51
+ if (IS_WINDOWS) {
52
+ try {
53
+ fs.unlinkSync(filePath);
54
+ } catch (err) {
55
+ if (err.code !== 'ENOENT') {
56
+ throw err;
57
+ }
58
+ // ENOENT = target doesn't exist yet, that's fine
59
+ }
60
+ }
61
+
62
+ // Step 3: Atomic rename
63
+ fs.renameSync(tmpPath, filePath);
64
+ } catch (error) {
65
+ // Clean up tmp file on failure
66
+ try {
67
+ fs.unlinkSync(tmpPath);
68
+ } catch (_cleanupErr) {
69
+ // Ignore cleanup errors
70
+ }
71
+
72
+ console.error(`[atomic-write] Failed to write ${filePath}: ${error.message}`);
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ module.exports = {
78
+ atomicWriteSync,
79
+ };