create-walle 0.9.11 → 0.9.13

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 (167) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -2
  3. package/template/bin/dev.sh +7 -1
  4. package/template/bin/setup.js +53 -9
  5. package/template/bin/sync-images.js +53 -0
  6. package/template/builder-journal.md +17 -0
  7. package/template/claude-task-manager/api-prompts.js +98 -13
  8. package/template/claude-task-manager/api-reviews.js +82 -5
  9. package/template/claude-task-manager/db.js +32 -5
  10. package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
  11. package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
  12. package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
  13. package/template/claude-task-manager/lib/session-capture.js +421 -0
  14. package/template/claude-task-manager/lib/session-history.js +135 -15
  15. package/template/claude-task-manager/lib/session-jobs.js +10 -5
  16. package/template/claude-task-manager/lib/session-stream.js +87 -19
  17. package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
  18. package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
  19. package/template/claude-task-manager/lib/walle-session-context.js +61 -0
  20. package/template/claude-task-manager/lib/walle-transcript.js +176 -0
  21. package/template/claude-task-manager/public/css/setup.css +35 -8
  22. package/template/claude-task-manager/public/css/walle-session.css +56 -0
  23. package/template/claude-task-manager/public/css/walle.css +120 -0
  24. package/template/claude-task-manager/public/index.html +814 -181
  25. package/template/claude-task-manager/public/js/message-renderer.js +148 -19
  26. package/template/claude-task-manager/public/js/reviews.js +120 -62
  27. package/template/claude-task-manager/public/js/setup.js +75 -31
  28. package/template/claude-task-manager/public/js/stream-view.js +115 -55
  29. package/template/claude-task-manager/public/js/walle-session.js +84 -2
  30. package/template/claude-task-manager/public/js/walle.js +308 -54
  31. package/template/claude-task-manager/server.js +1092 -146
  32. package/template/claude-task-manager/session-integrity.js +181 -54
  33. package/template/claude-task-manager/session-utils.js +123 -41
  34. package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
  35. package/template/package.json +1 -1
  36. package/template/wall-e/adapters/ctm.js +39 -18
  37. package/template/wall-e/agent-runners/contract.js +17 -0
  38. package/template/wall-e/agent-runners/index.js +22 -0
  39. package/template/wall-e/agent-runtime/harness.js +212 -0
  40. package/template/wall-e/agent-runtime/index.js +8 -0
  41. package/template/wall-e/agent-runtime/registry.js +67 -0
  42. package/template/wall-e/agent-runtime/session-store.js +179 -0
  43. package/template/wall-e/agent-runtime/spawn.js +208 -0
  44. package/template/wall-e/api-walle.js +174 -7
  45. package/template/wall-e/brain.js +266 -28
  46. package/template/wall-e/channels/policy.js +88 -0
  47. package/template/wall-e/channels/registry.js +15 -1
  48. package/template/wall-e/channels/reply-dispatcher.js +70 -0
  49. package/template/wall-e/channels/session-bindings.js +51 -0
  50. package/template/wall-e/chat/code-review-context.js +29 -0
  51. package/template/wall-e/chat.js +188 -42
  52. package/template/wall-e/coding/acp-adapter.js +188 -0
  53. package/template/wall-e/coding/agent-catalog.js +129 -0
  54. package/template/wall-e/coding/compaction-service.js +247 -0
  55. package/template/wall-e/coding/execution-trace.js +3 -0
  56. package/template/wall-e/coding/instruction-service.js +224 -0
  57. package/template/wall-e/coding/model-message.js +67 -0
  58. package/template/wall-e/coding/permission-rules-store.js +111 -0
  59. package/template/wall-e/coding/permission-service.js +266 -0
  60. package/template/wall-e/coding/prompt-bundle.js +67 -0
  61. package/template/wall-e/coding/prompt-runtime.js +243 -0
  62. package/template/wall-e/coding/provider-transform.js +188 -0
  63. package/template/wall-e/coding/runtime-mode.js +132 -0
  64. package/template/wall-e/coding/snapshot-service.js +155 -0
  65. package/template/wall-e/coding/stream-processor.js +268 -0
  66. package/template/wall-e/coding/task-tool.js +255 -0
  67. package/template/wall-e/coding/tool-registry.js +361 -0
  68. package/template/wall-e/coding/transcript-writer.js +143 -0
  69. package/template/wall-e/coding/workspace-replay.js +324 -0
  70. package/template/wall-e/coding-context.js +4 -22
  71. package/template/wall-e/coding-orchestrator.js +307 -18
  72. package/template/wall-e/coding-prompts.js +44 -3
  73. package/template/wall-e/context/context-builder.js +43 -1
  74. package/template/wall-e/context/topic-matcher.js +1 -1
  75. package/template/wall-e/eval/agent-runner.js +59 -13
  76. package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
  77. package/template/wall-e/eval/benchmarks.js +100 -16
  78. package/template/wall-e/eval/eval-orchestrator.js +218 -8
  79. package/template/wall-e/eval/harvester.js +62 -5
  80. package/template/wall-e/eval/head-to-head.js +23 -2
  81. package/template/wall-e/eval/humaneval-adapter.js +30 -5
  82. package/template/wall-e/eval/livecodebench-adapter.js +29 -5
  83. package/template/wall-e/eval/manifest.js +186 -0
  84. package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
  85. package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
  86. package/template/wall-e/eval/session-transcripts.js +57 -4
  87. package/template/wall-e/eval/swebench-adapter.js +109 -3
  88. package/template/wall-e/evaluation/agent-router.js +53 -1
  89. package/template/wall-e/evaluation/coding-quorum.js +48 -1
  90. package/template/wall-e/evaluation/router.js +4 -2
  91. package/template/wall-e/evaluation/tier-selector.js +11 -1
  92. package/template/wall-e/extraction/contradiction.js +2 -2
  93. package/template/wall-e/extraction/indexer.js +2 -1
  94. package/template/wall-e/extraction/knowledge-extractor.js +2 -2
  95. package/template/wall-e/hooks/cli.js +92 -0
  96. package/template/wall-e/hooks/discovery.js +119 -0
  97. package/template/wall-e/hooks/index.js +7 -0
  98. package/template/wall-e/hooks/manifest.js +55 -0
  99. package/template/wall-e/hooks/runtime.js +84 -0
  100. package/template/wall-e/hooks/session-memory.js +225 -0
  101. package/template/wall-e/http/auth.js +6 -2
  102. package/template/wall-e/http/chat-api.js +54 -8
  103. package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
  104. package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
  105. package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
  106. package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
  107. package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
  108. package/template/wall-e/listening/calendar.js +3 -1
  109. package/template/wall-e/llm/client.js +64 -10
  110. package/template/wall-e/llm/google.js +39 -5
  111. package/template/wall-e/llm/ollama.js +1 -1
  112. package/template/wall-e/llm/ollama.plugin.json +1 -1
  113. package/template/wall-e/llm/provider-availability.js +10 -0
  114. package/template/wall-e/llm/provider-error.js +269 -0
  115. package/template/wall-e/llm/tool-adapter.js +48 -12
  116. package/template/wall-e/loops/boot.js +2 -1
  117. package/template/wall-e/loops/initiative.js +2 -2
  118. package/template/wall-e/loops/tasks.js +8 -47
  119. package/template/wall-e/loops/workspace-prompts.js +20 -0
  120. package/template/wall-e/mcp-server.js +442 -1
  121. package/template/wall-e/memory/session-ingest-service.js +159 -0
  122. package/template/wall-e/memory/source-indexer.js +289 -0
  123. package/template/wall-e/plugins/discovery.js +83 -0
  124. package/template/wall-e/plugins/manifest-loader.js +50 -10
  125. package/template/wall-e/plugins/manifest-schema.js +69 -0
  126. package/template/wall-e/plugins/model-catalog.js +55 -0
  127. package/template/wall-e/prompts/coding/base.txt +2 -0
  128. package/template/wall-e/prompts/coding/deepseek.txt +1 -0
  129. package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
  130. package/template/wall-e/prompts/coding/plan.txt +1 -0
  131. package/template/wall-e/runtime/execution-trace.js +220 -0
  132. package/template/wall-e/security/audit.js +266 -0
  133. package/template/wall-e/security/ssrf.js +236 -0
  134. package/template/wall-e/session-files.js +303 -0
  135. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
  136. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
  137. package/template/wall-e/skills/internal-skill-registry.js +2 -2
  138. package/template/wall-e/skills/script-skill-runner.js +143 -0
  139. package/template/wall-e/skills/skill-executor.js +5 -6
  140. package/template/wall-e/skills/skill-fallback.js +3 -1
  141. package/template/wall-e/skills/skill-harness-registry.js +7 -8
  142. package/template/wall-e/skills/skill-planner.js +52 -4
  143. package/template/wall-e/skills/slack-ingest.js +11 -3
  144. package/template/wall-e/sources/base.js +90 -0
  145. package/template/wall-e/sources/builtin.js +33 -0
  146. package/template/wall-e/sources/claude-code-jsonl.js +78 -0
  147. package/template/wall-e/sources/codex-jsonl.js +125 -0
  148. package/template/wall-e/sources/coding-session-utils.js +117 -0
  149. package/template/wall-e/sources/contract-suite.js +59 -0
  150. package/template/wall-e/sources/gemini-jsonl.js +85 -0
  151. package/template/wall-e/sources/index.js +9 -0
  152. package/template/wall-e/sources/jsonl-utils.js +181 -0
  153. package/template/wall-e/sources/record-types.js +252 -0
  154. package/template/wall-e/sources/registry.js +92 -0
  155. package/template/wall-e/sources/transforms.js +100 -0
  156. package/template/wall-e/sources/walle-jsonl.js +108 -0
  157. package/template/wall-e/tools/coding-middleware.js +31 -1
  158. package/template/wall-e/tools/file-tracker.js +25 -1
  159. package/template/wall-e/tools/local-tools.js +75 -47
  160. package/template/wall-e/tools/session-sharing.js +68 -1
  161. package/template/wall-e/tools/shell-analyzer.js +1 -1
  162. package/template/wall-e/tools/shell-policy.js +47 -0
  163. package/template/wall-e/tools/snapshot.js +42 -0
  164. package/template/wall-e/training/harvester.js +62 -5
  165. package/template/wall-e/utils/repair.js +253 -1
  166. package/template/website/index.html +3 -3
  167. package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +0 -18
@@ -0,0 +1,324 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const crypto = require('node:crypto');
7
+
8
+ const DEFAULT_LIMIT_BYTES = 1024 * 1024;
9
+
10
+ class WorkspaceReplay {
11
+ constructor({ sessionsDir = defaultSessionsDir(), now = () => new Date().toISOString() } = {}) {
12
+ this.sessionsDir = sessionsDir;
13
+ this.now = now;
14
+ }
15
+
16
+ resolveSessionPath(sessionId, { transcriptPath = '' } = {}) {
17
+ return findSessionPath(sessionId, { transcriptPath, sessionsDir: this.sessionsDir });
18
+ }
19
+
20
+ appendRecord(sessionId, record = {}, opts = {}) {
21
+ const transcriptPath = opts.transcriptPath || sessionPath(this.sessionsDir, sessionId);
22
+ fs.mkdirSync(path.dirname(transcriptPath), { recursive: true });
23
+ const offset = fs.existsSync(transcriptPath) ? fs.statSync(transcriptPath).size : 0;
24
+ const normalized = {
25
+ provider: 'walle',
26
+ version: 'walle-jsonl-v1',
27
+ sessionId,
28
+ uuid: record.uuid || crypto.randomUUID(),
29
+ timestamp: record.timestamp || this.now(),
30
+ ...record,
31
+ };
32
+ const line = JSON.stringify(normalized) + '\n';
33
+ fs.appendFileSync(transcriptPath, line, 'utf8');
34
+ return {
35
+ sessionId,
36
+ transcriptPath,
37
+ offset,
38
+ bytes: Buffer.byteLength(line),
39
+ nextOffset: offset + Buffer.byteLength(line),
40
+ record: normalized,
41
+ };
42
+ }
43
+
44
+ appendPart(sessionId, partType, data = {}, opts = {}) {
45
+ return this.appendRecord(sessionId, {
46
+ type: 'walle_part',
47
+ cwd: opts.cwd || '',
48
+ chatSessionId: opts.chatSessionId || '',
49
+ parentUuid: opts.parentUuid || null,
50
+ partType: partType || 'event',
51
+ data: data && typeof data === 'object' ? data : {},
52
+ }, opts);
53
+ }
54
+
55
+ replayRange(sessionId, opts = {}) {
56
+ const transcriptPath = this.resolveSessionPath(sessionId, opts);
57
+ if (!transcriptPath) return emptyReplay(sessionId, opts);
58
+
59
+ const stat = fs.statSync(transcriptPath);
60
+ const startOffset = clampNumber(opts.offset, 0, stat.size);
61
+ const requestedSequence = clampNumber(opts.sequence, 0, Number.MAX_SAFE_INTEGER);
62
+ const limitBytes = Math.max(1, Number(opts.limitBytes || opts.limit || DEFAULT_LIMIT_BYTES));
63
+ const raw = readRange(transcriptPath, startOffset, limitBytes);
64
+ const parsed = parseJsonlRange(raw.text, startOffset, startOffset > 0 ? requestedSequence : 0);
65
+ const nextOffset = raw.nextOffset - Buffer.byteLength(parsed.tail, 'utf8');
66
+ const records = parsed.records.filter((entry) => startOffset > 0 || entry.sequence >= requestedSequence);
67
+
68
+ return {
69
+ sessionId,
70
+ transcriptPath,
71
+ offset: startOffset,
72
+ sequence: requestedSequence,
73
+ records,
74
+ nextOffset,
75
+ nextSequence: records.length ? records[records.length - 1].sequence + 1 : parsed.nextSequence,
76
+ fileSize: stat.size,
77
+ eof: nextOffset >= stat.size,
78
+ };
79
+ }
80
+
81
+ rebuildState(sessionId, opts = {}) {
82
+ const transcriptPath = this.resolveSessionPath(sessionId, opts);
83
+ if (!transcriptPath) return { ...emptyState(sessionId), workspace: workspaceStatus(opts.cwd || '') };
84
+
85
+ const replay = this.replayRange(sessionId, {
86
+ ...opts,
87
+ offset: 0,
88
+ sequence: 0,
89
+ limitBytes: Number.MAX_SAFE_INTEGER,
90
+ transcriptPath,
91
+ });
92
+ const state = emptyState(sessionId);
93
+ state.transcriptPath = transcriptPath;
94
+ state.nextOffset = replay.nextOffset;
95
+ state.nextSequence = replay.nextSequence;
96
+ state.fileSize = replay.fileSize;
97
+
98
+ for (const entry of replay.records) {
99
+ const record = entry.record;
100
+ if (record.type === 'session_meta') {
101
+ state.meta = {
102
+ ...state.meta,
103
+ ...pick(record, ['sessionId', 'chatSessionId', 'cwd', 'label', 'modelId', 'modelProvider', 'runtimeMode', 'gitBranch', 'agent', 'mode', 'timestamp']),
104
+ };
105
+ if (record.cwd) state.cwd = record.cwd;
106
+ continue;
107
+ }
108
+ if (record.cwd && !state.cwd) state.cwd = record.cwd;
109
+ if (record.type === 'user' || record.type === 'assistant') {
110
+ state.messages.push({
111
+ sequence: entry.sequence,
112
+ offset: entry.offset,
113
+ uuid: record.uuid || '',
114
+ parentUuid: record.parentUuid || null,
115
+ role: record.message?.role || record.type,
116
+ text: extractText(record.message?.content ?? record.message ?? record.content),
117
+ timestamp: record.timestamp || '',
118
+ });
119
+ continue;
120
+ }
121
+ if (record.type === 'walle_part') {
122
+ const part = {
123
+ sequence: entry.sequence,
124
+ offset: entry.offset,
125
+ uuid: record.uuid || '',
126
+ parentUuid: record.parentUuid || null,
127
+ partType: record.partType || 'event',
128
+ data: record.data || {},
129
+ timestamp: record.timestamp || '',
130
+ };
131
+ state.parts.push(part);
132
+ if (part.partType === 'tool' || part.partType === 'tool_call' || part.partType === 'tool_result') {
133
+ state.tools.push(part);
134
+ }
135
+ if (part.partType === 'permission' || part.data.state === 'permission_check') {
136
+ state.permissions.push(part);
137
+ }
138
+ if (part.partType === 'usage') {
139
+ state.usage.push(part.data);
140
+ }
141
+ }
142
+ }
143
+
144
+ state.workspace = workspaceStatus(opts.cwd || state.cwd || state.meta.cwd || '');
145
+ return state;
146
+ }
147
+ }
148
+
149
+ function defaultSessionsDir(env = process.env) {
150
+ if (env.WALLE_SESSIONS_DIR) return env.WALLE_SESSIONS_DIR;
151
+ if (env.WALL_E_SESSIONS_DIR) return env.WALL_E_SESSIONS_DIR;
152
+ if (env.WALLE_DEV_DIR) return path.join(env.WALLE_DEV_DIR, 'sessions');
153
+ if (env.WALL_E_DATA_DIR && env.WALL_E_DATA_DIR !== path.join(os.homedir(), '.walle', 'data')) {
154
+ return path.join(env.WALL_E_DATA_DIR, 'sessions');
155
+ }
156
+ if (env.CTM_DATA_DIR && env.CTM_DATA_DIR !== path.join(os.homedir(), '.walle', 'data')) {
157
+ return path.join(env.CTM_DATA_DIR, 'sessions');
158
+ }
159
+ return path.join(os.homedir(), '.walle', 'sessions');
160
+ }
161
+
162
+ function sessionPath(rootDir, sessionId) {
163
+ return path.join(rootDir, `${safeSessionId(sessionId)}.jsonl`);
164
+ }
165
+
166
+ function safeSessionId(sessionId) {
167
+ const id = String(sessionId || '');
168
+ if (!id || id.includes('..') || /[\\/]/.test(id) || !/^[A-Za-z0-9_.:-]+$/.test(id)) {
169
+ throw new Error(`Invalid session id: ${id || '<empty>'}`);
170
+ }
171
+ return id;
172
+ }
173
+
174
+ function findSessionPath(sessionId, { transcriptPath = '', sessionsDir = defaultSessionsDir() } = {}) {
175
+ if (transcriptPath && fs.existsSync(transcriptPath)) return transcriptPath;
176
+ if (!sessionId) return '';
177
+ const roots = [
178
+ sessionsDir,
179
+ process.env.WALLE_SESSIONS_DIR,
180
+ process.env.WALL_E_SESSIONS_DIR,
181
+ process.env.WALLE_DEV_DIR ? path.join(process.env.WALLE_DEV_DIR, 'sessions') : '',
182
+ process.env.WALL_E_DATA_DIR ? path.join(process.env.WALL_E_DATA_DIR, 'sessions') : '',
183
+ process.env.CTM_DATA_DIR ? path.join(process.env.CTM_DATA_DIR, 'sessions') : '',
184
+ path.join(os.homedir(), '.walle', 'sessions'),
185
+ ].filter(Boolean);
186
+ for (const root of [...new Set(roots)]) {
187
+ const candidate = sessionPath(root, sessionId);
188
+ try {
189
+ if (fs.existsSync(candidate)) return candidate;
190
+ } catch {}
191
+ }
192
+ return '';
193
+ }
194
+
195
+ function readRange(filePath, offset, limitBytes) {
196
+ const fd = fs.openSync(filePath, 'r');
197
+ try {
198
+ const stat = fs.fstatSync(fd);
199
+ const length = Math.min(limitBytes, Math.max(0, stat.size - offset));
200
+ const buffer = Buffer.alloc(length);
201
+ const bytesRead = fs.readSync(fd, buffer, 0, length, offset);
202
+ return {
203
+ text: buffer.toString('utf8', 0, bytesRead),
204
+ nextOffset: offset + bytesRead,
205
+ };
206
+ } finally {
207
+ fs.closeSync(fd);
208
+ }
209
+ }
210
+
211
+ function parseJsonlRange(text, baseOffset, baseSequence) {
212
+ const records = [];
213
+ const lines = text.split('\n');
214
+ const completeLineCount = text.endsWith('\n') ? lines.length - 1 : lines.length - 1;
215
+ let cursor = baseOffset;
216
+ let sequence = clampNumber(baseSequence, 0, Number.MAX_SAFE_INTEGER);
217
+ for (let i = 0; i < completeLineCount; i++) {
218
+ const line = lines[i];
219
+ const bytes = Buffer.byteLength(line + '\n');
220
+ if (line.trim()) {
221
+ try {
222
+ records.push({
223
+ sequence,
224
+ offset: cursor,
225
+ nextOffset: cursor + bytes,
226
+ record: JSON.parse(line),
227
+ raw: line,
228
+ });
229
+ sequence++;
230
+ } catch {
231
+ sequence++;
232
+ }
233
+ }
234
+ cursor += bytes;
235
+ }
236
+ return {
237
+ records,
238
+ tail: text.endsWith('\n') ? '' : lines[lines.length - 1],
239
+ nextSequence: sequence,
240
+ };
241
+ }
242
+
243
+ function emptyReplay(sessionId, opts = {}) {
244
+ return {
245
+ sessionId,
246
+ transcriptPath: '',
247
+ offset: clampNumber(opts.offset, 0, Number.MAX_SAFE_INTEGER),
248
+ sequence: clampNumber(opts.sequence, 0, Number.MAX_SAFE_INTEGER),
249
+ records: [],
250
+ nextOffset: 0,
251
+ nextSequence: clampNumber(opts.sequence, 0, Number.MAX_SAFE_INTEGER),
252
+ fileSize: 0,
253
+ eof: true,
254
+ };
255
+ }
256
+
257
+ function emptyState(sessionId) {
258
+ return {
259
+ sessionId,
260
+ transcriptPath: '',
261
+ cwd: '',
262
+ meta: {},
263
+ messages: [],
264
+ parts: [],
265
+ tools: [],
266
+ permissions: [],
267
+ usage: [],
268
+ workspace: workspaceStatus(''),
269
+ nextOffset: 0,
270
+ nextSequence: 0,
271
+ fileSize: 0,
272
+ };
273
+ }
274
+
275
+ function workspaceStatus(cwd) {
276
+ if (!cwd) return { cwd: '', available: false, reason: 'missing_cwd' };
277
+ try {
278
+ const stat = fs.statSync(cwd);
279
+ return { cwd, available: stat.isDirectory(), reason: stat.isDirectory() ? '' : 'not_directory' };
280
+ } catch {
281
+ return { cwd, available: false, reason: 'missing' };
282
+ }
283
+ }
284
+
285
+ function extractText(content) {
286
+ if (!content) return '';
287
+ if (typeof content === 'string') return content;
288
+ if (Array.isArray(content)) {
289
+ return content
290
+ .map((part) => {
291
+ if (!part) return '';
292
+ if (typeof part === 'string') return part;
293
+ return part.text || part.content || '';
294
+ })
295
+ .filter(Boolean)
296
+ .join('\n');
297
+ }
298
+ return content.text || content.content || '';
299
+ }
300
+
301
+ function pick(record, keys) {
302
+ const out = {};
303
+ for (const key of keys) {
304
+ if (record[key] !== undefined) out[key] = record[key];
305
+ }
306
+ return out;
307
+ }
308
+
309
+ function clampNumber(value, min, max) {
310
+ const n = Number(value);
311
+ if (!Number.isFinite(n)) return min;
312
+ return Math.min(max, Math.max(min, Math.floor(n)));
313
+ }
314
+
315
+ module.exports = {
316
+ WorkspaceReplay,
317
+ defaultSessionsDir,
318
+ sessionPath,
319
+ safeSessionId,
320
+ findSessionPath,
321
+ parseJsonlRange,
322
+ workspaceStatus,
323
+ extractText,
324
+ };
@@ -349,28 +349,10 @@ const INSTRUCTION_FILES = ['CLAUDE.md', 'AGENTS.md'];
349
349
  * @returns {Array<{ path: string, content: string }>}
350
350
  */
351
351
  function discoverInstructions(filePath, projectRoot) {
352
- const instructions = [];
353
- const resolvedRoot = path.resolve(projectRoot);
354
- let dir = path.dirname(path.resolve(filePath));
355
-
356
- while (dir.startsWith(resolvedRoot)) {
357
- for (const name of INSTRUCTION_FILES) {
358
- const candidate = path.join(dir, name);
359
- try {
360
- if (fs.existsSync(candidate)) {
361
- const content = fs.readFileSync(candidate, 'utf8');
362
- instructions.push({ path: candidate, content });
363
- }
364
- } catch {
365
- // Skip unreadable files
366
- }
367
- }
368
- const parent = path.dirname(dir);
369
- if (parent === dir) break; // filesystem root
370
- dir = parent;
371
- }
372
-
373
- return instructions;
352
+ const { InstructionService } = require('./coding/instruction-service');
353
+ return new InstructionService({ instructionFiles: INSTRUCTION_FILES })
354
+ .discoverForFile(filePath, projectRoot, { dedupe: false })
355
+ .instructions;
374
356
  }
375
357
 
376
358
  module.exports = {