claude-multi-session 1.0.1 → 2.3.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.
- package/README.md +453 -0
- package/STRATEGY.md +306 -0
- package/bin/cli.js +1083 -20
- package/bin/continuity-hook.js +118 -0
- package/bin/mcp.js +1 -1
- package/bin/setup.js +564 -315
- package/package.json +8 -3
- package/src/artifact-store.js +639 -0
- package/src/atomic-io.js +99 -0
- package/src/briefing-generator.js +451 -0
- package/src/continuity-hooks.js +253 -0
- package/src/contract-store.js +525 -0
- package/src/decision-journal.js +229 -0
- package/src/delegate.js +5 -16
- package/src/dependency-resolver.js +453 -0
- package/src/diff-engine.js +473 -0
- package/src/file-lock.js +161 -0
- package/src/index.js +26 -0
- package/src/lineage-graph.js +402 -0
- package/src/mcp-server.js +2073 -269
- package/src/pattern-registry.js +221 -0
- package/src/pipeline-engine.js +618 -0
- package/src/prompts.js +981 -0
- package/src/session-snapshot.js +508 -0
- package/src/snapshot-engine.js +490 -0
- package/src/stale-detector.js +169 -0
- package/src/team-hub.js +584 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision Journal — tracks architectural decisions, choices, and rationale
|
|
3
|
+
* Part of the Session Continuity Engine (Layer 0)
|
|
4
|
+
*
|
|
5
|
+
* This module helps maintain a record of important decisions made during
|
|
6
|
+
* development, including the reasoning behind them. This helps future sessions
|
|
7
|
+
* understand why certain choices were made.
|
|
8
|
+
*
|
|
9
|
+
* Storage: JSONL format (one JSON object per line)
|
|
10
|
+
* Location: ~/.claude-multi-session/continuity/{projectHash}/journal.jsonl
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const { readJsonSafe, appendJsonl } = require('./atomic-io');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* DecisionJournal class
|
|
21
|
+
*
|
|
22
|
+
* Manages a journal of decisions for a specific project.
|
|
23
|
+
* Each decision includes:
|
|
24
|
+
* - What was decided
|
|
25
|
+
* - Why it was decided
|
|
26
|
+
* - Related files
|
|
27
|
+
* - Tags for categorization
|
|
28
|
+
* - Additional context
|
|
29
|
+
*/
|
|
30
|
+
class DecisionJournal {
|
|
31
|
+
/**
|
|
32
|
+
* Create a new Decision Journal
|
|
33
|
+
*
|
|
34
|
+
* @param {string} projectPath - Absolute path to the project directory
|
|
35
|
+
*
|
|
36
|
+
* The journal is stored in a directory based on a hash of the project path,
|
|
37
|
+
* so each project gets its own isolated journal.
|
|
38
|
+
*/
|
|
39
|
+
constructor(projectPath) {
|
|
40
|
+
// Create a unique hash for this project (16 characters)
|
|
41
|
+
// This ensures each project has its own journal storage
|
|
42
|
+
const projectHash = crypto
|
|
43
|
+
.createHash('sha256')
|
|
44
|
+
.update(projectPath)
|
|
45
|
+
.digest('hex')
|
|
46
|
+
.slice(0, 16);
|
|
47
|
+
|
|
48
|
+
// Set up storage directory: ~/.claude-multi-session/continuity/{hash}
|
|
49
|
+
this.storageDir = path.join(
|
|
50
|
+
os.homedir(),
|
|
51
|
+
'.claude-multi-session',
|
|
52
|
+
'continuity',
|
|
53
|
+
projectHash
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// The journal file is a JSONL file (one JSON object per line)
|
|
57
|
+
this.journalPath = path.join(this.storageDir, 'journal.jsonl');
|
|
58
|
+
|
|
59
|
+
// Create the storage directory if it doesn't exist yet
|
|
60
|
+
if (!fs.existsSync(this.storageDir)) {
|
|
61
|
+
fs.mkdirSync(this.storageDir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Add a new decision to the journal
|
|
67
|
+
*
|
|
68
|
+
* @param {Object} params - Decision parameters
|
|
69
|
+
* @param {string} params.decision - The decision that was made (required)
|
|
70
|
+
* @param {string} [params.reason] - Why this decision was made
|
|
71
|
+
* @param {string[]} [params.files] - Related file paths
|
|
72
|
+
* @param {string[]} [params.tags] - Tags for categorization (e.g., ['auth', 'security'])
|
|
73
|
+
* @param {string} [params.context] - Additional context or notes
|
|
74
|
+
* @returns {Object} The created decision entry with ID and timestamp
|
|
75
|
+
*
|
|
76
|
+
* Example:
|
|
77
|
+
* journal.add({
|
|
78
|
+
* decision: 'Use bcrypt for password hashing',
|
|
79
|
+
* reason: 'More mature and widely tested than argon2',
|
|
80
|
+
* files: ['src/auth.js'],
|
|
81
|
+
* tags: ['security', 'auth']
|
|
82
|
+
* })
|
|
83
|
+
*/
|
|
84
|
+
add({ decision, reason, files, tags, context }) {
|
|
85
|
+
// Generate a unique ID for this decision
|
|
86
|
+
// Format: d_{timestamp}_{random} (e.g., d_1707840000000_a3f2b1c4)
|
|
87
|
+
const id = 'd_' + Date.now() + '_' + crypto.randomBytes(4).toString('hex');
|
|
88
|
+
|
|
89
|
+
// Build the decision entry
|
|
90
|
+
const entry = {
|
|
91
|
+
id,
|
|
92
|
+
decision, // The decision itself (required)
|
|
93
|
+
reason: reason || '', // Why it was made (optional)
|
|
94
|
+
files: files || [], // Related files (optional)
|
|
95
|
+
tags: tags || [], // Tags for filtering (optional)
|
|
96
|
+
context: context || '', // Extra context (optional)
|
|
97
|
+
createdAt: new Date().toISOString() // When it was created
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Append to the journal file (one line per decision)
|
|
101
|
+
appendJsonl(this.journalPath, entry);
|
|
102
|
+
|
|
103
|
+
// Return the entry so the caller knows what was saved
|
|
104
|
+
return entry;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* List decisions from the journal
|
|
109
|
+
*
|
|
110
|
+
* @param {Object} [options] - Filter and pagination options
|
|
111
|
+
* @param {number} [options.limit=50] - Maximum number of decisions to return
|
|
112
|
+
* @param {string} [options.tag] - Only return decisions with this tag
|
|
113
|
+
* @param {string} [options.since] - Only return decisions after this date (ISO string)
|
|
114
|
+
* @returns {Object[]} Array of decision entries, newest first
|
|
115
|
+
*
|
|
116
|
+
* Example:
|
|
117
|
+
* journal.list({ limit: 10, tag: 'auth' })
|
|
118
|
+
* journal.list({ since: '2024-01-01T00:00:00Z' })
|
|
119
|
+
*/
|
|
120
|
+
list({ limit = 50, tag, since } = {}) {
|
|
121
|
+
// If the journal file doesn't exist yet, return empty array
|
|
122
|
+
if (!fs.existsSync(this.journalPath)) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Read the entire journal file
|
|
127
|
+
const content = fs.readFileSync(this.journalPath, 'utf-8');
|
|
128
|
+
|
|
129
|
+
// Parse each line as JSON (JSONL format)
|
|
130
|
+
const entries = content
|
|
131
|
+
.split('\n') // Split into lines
|
|
132
|
+
.filter(line => line.trim()) // Remove empty lines
|
|
133
|
+
.map(line => JSON.parse(line)); // Parse each line as JSON
|
|
134
|
+
|
|
135
|
+
// Start with all entries
|
|
136
|
+
let filtered = entries;
|
|
137
|
+
|
|
138
|
+
// Filter by tag if specified
|
|
139
|
+
if (tag) {
|
|
140
|
+
filtered = filtered.filter(entry => entry.tags.includes(tag));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Filter by date if specified
|
|
144
|
+
if (since) {
|
|
145
|
+
const sinceDate = new Date(since);
|
|
146
|
+
filtered = filtered.filter(entry => new Date(entry.createdAt) >= sinceDate);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Sort by creation date, newest first
|
|
150
|
+
// (Reverse because JSONL is in chronological order)
|
|
151
|
+
filtered.reverse();
|
|
152
|
+
|
|
153
|
+
// Return only the requested number of entries
|
|
154
|
+
return filtered.slice(0, limit);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Search for decisions by keyword
|
|
159
|
+
*
|
|
160
|
+
* @param {string} keyword - Search term (case-insensitive)
|
|
161
|
+
* @returns {Object[]} Array of matching decision entries, newest first
|
|
162
|
+
*
|
|
163
|
+
* Searches across the decision text, reason, and context fields.
|
|
164
|
+
*
|
|
165
|
+
* Example:
|
|
166
|
+
* journal.search('bcrypt')
|
|
167
|
+
* journal.search('authentication')
|
|
168
|
+
*/
|
|
169
|
+
search(keyword) {
|
|
170
|
+
// If the journal file doesn't exist yet, return empty array
|
|
171
|
+
if (!fs.existsSync(this.journalPath)) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Read and parse all entries (same as list())
|
|
176
|
+
const content = fs.readFileSync(this.journalPath, 'utf-8');
|
|
177
|
+
const entries = content
|
|
178
|
+
.split('\n')
|
|
179
|
+
.filter(line => line.trim())
|
|
180
|
+
.map(line => JSON.parse(line));
|
|
181
|
+
|
|
182
|
+
// Convert keyword to lowercase for case-insensitive search
|
|
183
|
+
const searchTerm = keyword.toLowerCase();
|
|
184
|
+
|
|
185
|
+
// Find entries where the keyword appears in decision, reason, or context
|
|
186
|
+
const matches = entries.filter(entry => {
|
|
187
|
+
return (
|
|
188
|
+
entry.decision.toLowerCase().includes(searchTerm) ||
|
|
189
|
+
entry.reason.toLowerCase().includes(searchTerm) ||
|
|
190
|
+
entry.context.toLowerCase().includes(searchTerm)
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Return matches, newest first
|
|
195
|
+
return matches.reverse();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get a specific decision by its ID
|
|
200
|
+
*
|
|
201
|
+
* @param {string} decisionId - The decision ID to retrieve
|
|
202
|
+
* @returns {Object|null} The decision entry, or null if not found
|
|
203
|
+
*
|
|
204
|
+
* Example:
|
|
205
|
+
* journal.get('d_1707840000000_a3f2b1c4')
|
|
206
|
+
*/
|
|
207
|
+
get(decisionId) {
|
|
208
|
+
// If the journal file doesn't exist yet, return null
|
|
209
|
+
if (!fs.existsSync(this.journalPath)) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Read and parse all entries
|
|
214
|
+
const content = fs.readFileSync(this.journalPath, 'utf-8');
|
|
215
|
+
const entries = content
|
|
216
|
+
.split('\n')
|
|
217
|
+
.filter(line => line.trim())
|
|
218
|
+
.map(line => JSON.parse(line));
|
|
219
|
+
|
|
220
|
+
// Find the entry with matching ID
|
|
221
|
+
const entry = entries.find(e => e.id === decisionId);
|
|
222
|
+
|
|
223
|
+
// Return the entry or null if not found
|
|
224
|
+
return entry || null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Export the class so other modules can use it
|
|
229
|
+
module.exports = DecisionJournal;
|
package/src/delegate.js
CHANGED
|
@@ -119,7 +119,7 @@ class Delegate {
|
|
|
119
119
|
: null;
|
|
120
120
|
|
|
121
121
|
// Build the full prompt with context and instructions
|
|
122
|
-
const fullPrompt = this._buildPrompt(task, options.context);
|
|
122
|
+
const fullPrompt = this._buildPrompt(task, options.context, name);
|
|
123
123
|
|
|
124
124
|
// Spawn the session
|
|
125
125
|
let spawnResult;
|
|
@@ -248,22 +248,11 @@ class Delegate {
|
|
|
248
248
|
|
|
249
249
|
/**
|
|
250
250
|
* Build the full prompt with context and structured output instructions.
|
|
251
|
+
* Uses the production-quality delegate prompt template from prompts.js.
|
|
251
252
|
*/
|
|
252
|
-
_buildPrompt(task, context) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (context) {
|
|
256
|
-
prompt += `CONTEXT:\n${context}\n\n`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
prompt += `TASK:\n${task}\n\n`;
|
|
260
|
-
prompt += `INSTRUCTIONS:\n`;
|
|
261
|
-
prompt += `- Complete the task thoroughly\n`;
|
|
262
|
-
prompt += `- If you encounter errors, try to fix them\n`;
|
|
263
|
-
prompt += `- If you need clarification, say what you need and stop\n`;
|
|
264
|
-
prompt += `- At the end, provide a brief summary of what you did\n`;
|
|
265
|
-
|
|
266
|
-
return prompt;
|
|
253
|
+
_buildPrompt(task, context, name) {
|
|
254
|
+
const { buildDelegatePrompt } = require('./prompts');
|
|
255
|
+
return buildDelegatePrompt(task, context, name);
|
|
267
256
|
}
|
|
268
257
|
|
|
269
258
|
/**
|