@straiffi/archon 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.
- package/README.md +224 -0
- package/dist/cli.js +216 -0
- package/dist/client/assets/index-8_-boBBA.css +2 -0
- package/dist/client/assets/index-s_jjeqha.js +176 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/client/favicon.svg +62 -0
- package/dist/client/icons.svg +24 -0
- package/dist/client/index.html +14 -0
- package/dist/server/db.js +764 -0
- package/dist/server/db.js.map +1 -0
- package/dist/server/index.js +5134 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/lib/agent.js +1302 -0
- package/dist/server/lib/agent.js.map +1 -0
- package/dist/server/lib/buildChains.js +2 -0
- package/dist/server/lib/buildChains.js.map +1 -0
- package/dist/server/lib/buildFlow.js +59 -0
- package/dist/server/lib/buildFlow.js.map +1 -0
- package/dist/server/lib/buildSequences.js +599 -0
- package/dist/server/lib/buildSequences.js.map +1 -0
- package/dist/server/lib/bundleActivity.js +95 -0
- package/dist/server/lib/bundleActivity.js.map +1 -0
- package/dist/server/lib/bundlePullRequests.js +126 -0
- package/dist/server/lib/bundlePullRequests.js.map +1 -0
- package/dist/server/lib/chatMessages.js +60 -0
- package/dist/server/lib/chatMessages.js.map +1 -0
- package/dist/server/lib/chatTargets.js +123 -0
- package/dist/server/lib/chatTargets.js.map +1 -0
- package/dist/server/lib/chatTicketProposals.js +180 -0
- package/dist/server/lib/chatTicketProposals.js.map +1 -0
- package/dist/server/lib/chats.js +279 -0
- package/dist/server/lib/chats.js.map +1 -0
- package/dist/server/lib/config.js +3 -0
- package/dist/server/lib/config.js.map +1 -0
- package/dist/server/lib/cors.js +30 -0
- package/dist/server/lib/cors.js.map +1 -0
- package/dist/server/lib/directoryPicker.js +174 -0
- package/dist/server/lib/directoryPicker.js.map +1 -0
- package/dist/server/lib/git.js +1284 -0
- package/dist/server/lib/git.js.map +1 -0
- package/dist/server/lib/integrations/github.js +511 -0
- package/dist/server/lib/integrations/github.js.map +1 -0
- package/dist/server/lib/integrations/index.js +162 -0
- package/dist/server/lib/integrations/index.js.map +1 -0
- package/dist/server/lib/integrations/jira.js +283 -0
- package/dist/server/lib/integrations/jira.js.map +1 -0
- package/dist/server/lib/integrations/planning.js +27 -0
- package/dist/server/lib/integrations/planning.js.map +1 -0
- package/dist/server/lib/integrations/types.js +2 -0
- package/dist/server/lib/integrations/types.js.map +1 -0
- package/dist/server/lib/lightweightPrompt.js +88 -0
- package/dist/server/lib/lightweightPrompt.js.map +1 -0
- package/dist/server/lib/models.js +219 -0
- package/dist/server/lib/models.js.map +1 -0
- package/dist/server/lib/preview.js +377 -0
- package/dist/server/lib/preview.js.map +1 -0
- package/dist/server/lib/previewProxy.js +659 -0
- package/dist/server/lib/previewProxy.js.map +1 -0
- package/dist/server/lib/projectAutoConfig.js +682 -0
- package/dist/server/lib/projectAutoConfig.js.map +1 -0
- package/dist/server/lib/projectFileSuggestions.js +133 -0
- package/dist/server/lib/projectFileSuggestions.js.map +1 -0
- package/dist/server/lib/projectMemory.js +1519 -0
- package/dist/server/lib/projectMemory.js.map +1 -0
- package/dist/server/lib/projectMemoryPrompt.js +390 -0
- package/dist/server/lib/projectMemoryPrompt.js.map +1 -0
- package/dist/server/lib/projectMemoryScan.js +681 -0
- package/dist/server/lib/projectMemoryScan.js.map +1 -0
- package/dist/server/lib/projectMemorySuggestions.js +166 -0
- package/dist/server/lib/projectMemorySuggestions.js.map +1 -0
- package/dist/server/lib/projectMemoryTransfer.js +958 -0
- package/dist/server/lib/projectMemoryTransfer.js.map +1 -0
- package/dist/server/lib/projects.js +569 -0
- package/dist/server/lib/projects.js.map +1 -0
- package/dist/server/lib/promptSkills.js +28 -0
- package/dist/server/lib/promptSkills.js.map +1 -0
- package/dist/server/lib/queue.js +15 -0
- package/dist/server/lib/queue.js.map +1 -0
- package/dist/server/lib/reviewFindings.js +390 -0
- package/dist/server/lib/reviewFindings.js.map +1 -0
- package/dist/server/lib/run.js +416 -0
- package/dist/server/lib/run.js.map +1 -0
- package/dist/server/lib/runtimePaths.js +93 -0
- package/dist/server/lib/runtimePaths.js.map +1 -0
- package/dist/server/lib/shell.js +27 -0
- package/dist/server/lib/shell.js.map +1 -0
- package/dist/server/lib/skills.js +124 -0
- package/dist/server/lib/skills.js.map +1 -0
- package/dist/server/lib/startDev.js +18 -0
- package/dist/server/lib/startDev.js.map +1 -0
- package/dist/server/lib/staticClient.js +80 -0
- package/dist/server/lib/staticClient.js.map +1 -0
- package/dist/server/lib/terminal.js +366 -0
- package/dist/server/lib/terminal.js.map +1 -0
- package/dist/server/lib/ticketDependencies.js +174 -0
- package/dist/server/lib/ticketDependencies.js.map +1 -0
- package/dist/server/lib/ticketMessages.js +65 -0
- package/dist/server/lib/ticketMessages.js.map +1 -0
- package/dist/server/lib/ticketOpenQuestions.js +128 -0
- package/dist/server/lib/ticketOpenQuestions.js.map +1 -0
- package/dist/server/lib/ticketUndo.js +549 -0
- package/dist/server/lib/ticketUndo.js.map +1 -0
- package/dist/server/lib/tickets.js +981 -0
- package/dist/server/lib/tickets.js.map +1 -0
- package/dist/server/lib/types.js +2 -0
- package/dist/server/lib/types.js.map +1 -0
- package/dist/server/package.json +3 -0
- package/dist/server/workers/build.js +229 -0
- package/dist/server/workers/build.js.map +1 -0
- package/dist/server/workers/chat.js +190 -0
- package/dist/server/workers/chat.js.map +1 -0
- package/dist/server/workers/followUp.js +204 -0
- package/dist/server/workers/followUp.js.map +1 -0
- package/dist/server/workers/plan.js +1130 -0
- package/dist/server/workers/plan.js.map +1 -0
- package/dist/server/workers/planFollowUp.js +360 -0
- package/dist/server/workers/planFollowUp.js.map +1 -0
- package/dist/server/workers/review.js +167 -0
- package/dist/server/workers/review.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { existsSync, lstatSync, readdirSync, readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import config from './config.js';
|
|
5
|
+
import { runLightweightPrompt } from './lightweightPrompt.js';
|
|
6
|
+
import { createProjectContextScan, getActiveProjectContextScan, getLatestProjectContextScan, getLatestSuccessfulProjectContextScan, replaceProjectContextArtifacts, updateProjectContextScan, } from './projectMemory.js';
|
|
7
|
+
import { getProjectById } from './projects.js';
|
|
8
|
+
import { enqueue, isRunning } from './queue.js';
|
|
9
|
+
const SCAN_JOB_PREFIX = 'project-context-scan';
|
|
10
|
+
const TOP_LEVEL_TEXT_FILES = ['AGENTS.md', 'CLAUDE.md', 'package.json', 'pnpm-workspace.yaml', 'turbo.json', 'tsconfig.json', 'pyproject.toml', 'Cargo.toml', 'go.mod', 'composer.json'];
|
|
11
|
+
const HIGH_VALUE_DIRECTORIES = ['src', 'app', 'client', 'server', 'packages', 'libs', 'components', 'api', 'docs', 'tests'];
|
|
12
|
+
const MAX_TOP_LEVEL_ENTRIES = 40;
|
|
13
|
+
const MAX_FILE_CHARS = 4_000;
|
|
14
|
+
const MAX_DIRECTORY_CHILDREN = 8;
|
|
15
|
+
const MAX_HIGH_VALUE_DIRECTORIES = 6;
|
|
16
|
+
const MAX_DIRECTORY_DEPTH = 4;
|
|
17
|
+
const MAX_DIRECTORY_SNAPSHOT_LINES = 80;
|
|
18
|
+
const MAX_VISIBLE_TEST_FILES = 32;
|
|
19
|
+
const MAX_REPRESENTATIVE_SOURCE_FILES = 12;
|
|
20
|
+
const MAX_REPRESENTATIVE_SOURCE_FILES_PER_DIRECTORY = 2;
|
|
21
|
+
const MAX_REPRESENTATIVE_TEST_FILES = 2;
|
|
22
|
+
const MAX_REPRESENTATIVE_FILE_CHARS = 2_500;
|
|
23
|
+
const MAX_REPRESENTATIVE_DIRECTORY_CHILDREN = 16;
|
|
24
|
+
const MAX_REPRESENTATIVE_CANDIDATES = 160;
|
|
25
|
+
const STALE_SCAN_WARNING_AGE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
26
|
+
const IGNORED_DIRECTORY_NAMES = new Set(['.git', '.next', '.turbo', 'build', 'coverage', 'dist', 'node_modules', 'tmp']);
|
|
27
|
+
const TEST_CONFIG_FILE_NAMES = new Set(['jest.config.js', 'jest.config.ts', 'playwright.config.js', 'playwright.config.ts', 'vitest.config.js', 'vitest.config.ts']);
|
|
28
|
+
const PRIORITY_DIRECTORY_NAMES = new Set([
|
|
29
|
+
'__tests__',
|
|
30
|
+
'api',
|
|
31
|
+
'app',
|
|
32
|
+
'client',
|
|
33
|
+
'components',
|
|
34
|
+
'hooks',
|
|
35
|
+
'lib',
|
|
36
|
+
'pages',
|
|
37
|
+
'routes',
|
|
38
|
+
'server',
|
|
39
|
+
'src',
|
|
40
|
+
'tests',
|
|
41
|
+
'workers',
|
|
42
|
+
'workspace',
|
|
43
|
+
]);
|
|
44
|
+
const PRIORITY_FILE_PATTERNS = [
|
|
45
|
+
/^(main|index|app|server|client)\.[^.]+$/i,
|
|
46
|
+
/^route(?:r|s)?\.[^.]+$/i,
|
|
47
|
+
/^vite\.config\.[^.]+$/i,
|
|
48
|
+
/^vitest\.config\.[^.]+$/i,
|
|
49
|
+
/^jest\.config\.[^.]+$/i,
|
|
50
|
+
/^playwright\.config\.[^.]+$/i,
|
|
51
|
+
/^package\.json$/i,
|
|
52
|
+
/^tsconfig(?:\.[^.]+)?\.json$/i,
|
|
53
|
+
];
|
|
54
|
+
const REPRESENTATIVE_SOURCE_FILE_PATTERN = /\.(ts|tsx|js|jsx|mjs|cjs|mts|cts|py|go|rs|java|kt|kts|php|rb|swift|vue|svelte)$/i;
|
|
55
|
+
const getScanJobKey = (projectId) => `${SCAN_JOB_PREFIX}:${projectId}`;
|
|
56
|
+
const truncateText = (value, limit) => {
|
|
57
|
+
if (value.length <= limit) {
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
return `${value.slice(0, limit).trimEnd()}\n...[truncated]`;
|
|
61
|
+
};
|
|
62
|
+
const safeListEntries = (directoryPath) => {
|
|
63
|
+
try {
|
|
64
|
+
return readdirSync(directoryPath).sort((left, right) => left.localeCompare(right));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const safeReadTextFile = (filePath, limit = MAX_FILE_CHARS) => {
|
|
71
|
+
try {
|
|
72
|
+
return truncateText(readFileSync(filePath, 'utf8'), limit);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const isDirectory = (targetPath) => {
|
|
79
|
+
try {
|
|
80
|
+
return lstatSync(targetPath).isDirectory();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const findReadme = (entries) => {
|
|
87
|
+
return entries.find(entry => {
|
|
88
|
+
const normalized = entry.toLowerCase();
|
|
89
|
+
return normalized === 'readme' || normalized.startsWith('readme.');
|
|
90
|
+
}) ?? null;
|
|
91
|
+
};
|
|
92
|
+
const isIgnoredDirectoryName = (entryName) => IGNORED_DIRECTORY_NAMES.has(entryName.toLowerCase());
|
|
93
|
+
const scorePathPriority = (relativePath, isDirectoryEntry) => {
|
|
94
|
+
const normalized = relativePath.toLowerCase();
|
|
95
|
+
const segments = normalized.split('/');
|
|
96
|
+
const name = segments[segments.length - 1] ?? normalized;
|
|
97
|
+
let score = 0;
|
|
98
|
+
if (isDirectoryEntry) {
|
|
99
|
+
if (PRIORITY_DIRECTORY_NAMES.has(name)) {
|
|
100
|
+
score += 90;
|
|
101
|
+
}
|
|
102
|
+
if (name === '__tests__' || name === 'tests') {
|
|
103
|
+
score += 30;
|
|
104
|
+
}
|
|
105
|
+
if (segments.length <= 2) {
|
|
106
|
+
score += 12;
|
|
107
|
+
}
|
|
108
|
+
return score;
|
|
109
|
+
}
|
|
110
|
+
for (const pattern of PRIORITY_FILE_PATTERNS) {
|
|
111
|
+
if (pattern.test(name)) {
|
|
112
|
+
score += 120;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (REPRESENTATIVE_SOURCE_FILE_PATTERN.test(name)) {
|
|
117
|
+
score += 30;
|
|
118
|
+
}
|
|
119
|
+
if (isVisibleTestFile(relativePath)) {
|
|
120
|
+
score += 120;
|
|
121
|
+
}
|
|
122
|
+
if (normalized.startsWith('client/src/')) {
|
|
123
|
+
score += 35;
|
|
124
|
+
}
|
|
125
|
+
if (normalized.startsWith('server/')) {
|
|
126
|
+
score += 35;
|
|
127
|
+
}
|
|
128
|
+
if (normalized.includes('/workers/')) {
|
|
129
|
+
score += 20;
|
|
130
|
+
}
|
|
131
|
+
if (normalized.includes('/api/')) {
|
|
132
|
+
score += 18;
|
|
133
|
+
}
|
|
134
|
+
if (normalized.includes('/routes/')) {
|
|
135
|
+
score += 18;
|
|
136
|
+
}
|
|
137
|
+
if (normalized.includes('/lib/')) {
|
|
138
|
+
score += 16;
|
|
139
|
+
}
|
|
140
|
+
if (normalized.includes('/components/')) {
|
|
141
|
+
score += 12;
|
|
142
|
+
}
|
|
143
|
+
if (segments.length <= 3) {
|
|
144
|
+
score += 14;
|
|
145
|
+
}
|
|
146
|
+
return score;
|
|
147
|
+
};
|
|
148
|
+
const listPrioritizedEntries = (directoryPath, relativeDirectoryPath, limit) => {
|
|
149
|
+
return safeListEntries(directoryPath)
|
|
150
|
+
.filter(entry => !isIgnoredDirectoryName(entry))
|
|
151
|
+
.map(entry => {
|
|
152
|
+
const path = join(directoryPath, entry);
|
|
153
|
+
const relativePath = relativeDirectoryPath ? `${relativeDirectoryPath}/${entry}` : entry;
|
|
154
|
+
const directory = isDirectory(path);
|
|
155
|
+
return {
|
|
156
|
+
name: entry,
|
|
157
|
+
path,
|
|
158
|
+
relativePath,
|
|
159
|
+
isDirectory: directory,
|
|
160
|
+
score: scorePathPriority(relativePath, directory),
|
|
161
|
+
};
|
|
162
|
+
})
|
|
163
|
+
.sort((left, right) => right.score - left.score || left.name.localeCompare(right.name))
|
|
164
|
+
.slice(0, limit);
|
|
165
|
+
};
|
|
166
|
+
const isVisibleTestFile = (relativePath) => {
|
|
167
|
+
const normalized = relativePath.toLowerCase();
|
|
168
|
+
const segments = normalized.split('/');
|
|
169
|
+
const fileName = segments[segments.length - 1] ?? normalized;
|
|
170
|
+
return TEST_CONFIG_FILE_NAMES.has(fileName)
|
|
171
|
+
|| normalized.includes('/__tests__/')
|
|
172
|
+
|| normalized.startsWith('__tests__/')
|
|
173
|
+
|| normalized.startsWith('tests/')
|
|
174
|
+
|| /\.(test|spec)\.[^.]+$/.test(fileName);
|
|
175
|
+
};
|
|
176
|
+
const isRepresentativeSourceFile = (relativePath) => {
|
|
177
|
+
const normalized = relativePath.toLowerCase();
|
|
178
|
+
const fileName = normalized.split('/').at(-1) ?? normalized;
|
|
179
|
+
return !isVisibleTestFile(relativePath)
|
|
180
|
+
&& (REPRESENTATIVE_SOURCE_FILE_PATTERN.test(fileName) || fileName === 'package.json');
|
|
181
|
+
};
|
|
182
|
+
const collectDirectoryFileCandidates = (projectPath, directoryName) => {
|
|
183
|
+
const directoryPath = join(projectPath, directoryName);
|
|
184
|
+
if (!isDirectory(directoryPath)) {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
const matches = [];
|
|
188
|
+
const seen = new Set();
|
|
189
|
+
const visitDirectory = (currentPath, relativePath, depth) => {
|
|
190
|
+
if (depth >= MAX_DIRECTORY_DEPTH || matches.length >= MAX_REPRESENTATIVE_CANDIDATES) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const entries = listPrioritizedEntries(currentPath, relativePath, MAX_REPRESENTATIVE_DIRECTORY_CHILDREN);
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
if (entry.isDirectory) {
|
|
196
|
+
visitDirectory(entry.path, entry.relativePath, depth + 1);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (matches.length >= MAX_REPRESENTATIVE_CANDIDATES || seen.has(entry.relativePath)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
seen.add(entry.relativePath);
|
|
203
|
+
matches.push(entry.relativePath);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
visitDirectory(directoryPath, directoryName, 0);
|
|
207
|
+
return matches;
|
|
208
|
+
};
|
|
209
|
+
const selectRepresentativeFiles = (projectPath, directoryNames, predicate, maxTotal, maxPerDirectory) => {
|
|
210
|
+
const selected = [];
|
|
211
|
+
const seen = new Set();
|
|
212
|
+
const overflow = [];
|
|
213
|
+
for (const directoryName of directoryNames) {
|
|
214
|
+
const directoryMatches = collectDirectoryFileCandidates(projectPath, directoryName)
|
|
215
|
+
.filter(predicate)
|
|
216
|
+
.map(relativePath => ({
|
|
217
|
+
name: relativePath.split('/').at(-1) ?? relativePath,
|
|
218
|
+
path: join(projectPath, relativePath),
|
|
219
|
+
relativePath,
|
|
220
|
+
isDirectory: false,
|
|
221
|
+
score: scorePathPriority(relativePath, false),
|
|
222
|
+
}))
|
|
223
|
+
.sort((left, right) => right.score - left.score || left.relativePath.localeCompare(right.relativePath));
|
|
224
|
+
for (const entry of directoryMatches.slice(0, maxPerDirectory)) {
|
|
225
|
+
if (selected.length >= maxTotal || seen.has(entry.relativePath)) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
seen.add(entry.relativePath);
|
|
229
|
+
selected.push(entry.relativePath);
|
|
230
|
+
}
|
|
231
|
+
overflow.push(...directoryMatches.slice(maxPerDirectory));
|
|
232
|
+
}
|
|
233
|
+
overflow.sort((left, right) => right.score - left.score || left.relativePath.localeCompare(right.relativePath));
|
|
234
|
+
for (const entry of overflow) {
|
|
235
|
+
if (selected.length >= maxTotal || seen.has(entry.relativePath)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
seen.add(entry.relativePath);
|
|
239
|
+
selected.push(entry.relativePath);
|
|
240
|
+
}
|
|
241
|
+
return selected;
|
|
242
|
+
};
|
|
243
|
+
const appendDirectorySnapshot = (lines, directoryPath, relativeDirectoryPath, depth, budget) => {
|
|
244
|
+
if (depth >= MAX_DIRECTORY_DEPTH || budget.remaining <= 0) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const childEntries = listPrioritizedEntries(directoryPath, relativeDirectoryPath, MAX_DIRECTORY_CHILDREN);
|
|
248
|
+
for (const entry of childEntries) {
|
|
249
|
+
if (budget.remaining <= 0) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
lines.push(`${' '.repeat(depth)}- ${entry.name}${entry.isDirectory ? '/' : ''}`);
|
|
253
|
+
budget.remaining -= 1;
|
|
254
|
+
if (entry.isDirectory) {
|
|
255
|
+
appendDirectorySnapshot(lines, entry.path, entry.relativePath, depth + 1, budget);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const collectVisibleTestFiles = (projectPath, directoryNames) => {
|
|
260
|
+
const matches = [];
|
|
261
|
+
const seen = new Set();
|
|
262
|
+
const addMatch = (relativePath) => {
|
|
263
|
+
if (!isVisibleTestFile(relativePath) || seen.has(relativePath) || matches.length >= MAX_VISIBLE_TEST_FILES) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
seen.add(relativePath);
|
|
267
|
+
matches.push(relativePath);
|
|
268
|
+
};
|
|
269
|
+
for (const entry of safeListEntries(projectPath)) {
|
|
270
|
+
if (!isDirectory(join(projectPath, entry))) {
|
|
271
|
+
addMatch(entry);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const visitDirectory = (directoryPath, relativePath, depth) => {
|
|
275
|
+
if (depth >= MAX_DIRECTORY_DEPTH || matches.length >= MAX_VISIBLE_TEST_FILES) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const childEntries = listPrioritizedEntries(directoryPath, relativePath, MAX_DIRECTORY_CHILDREN);
|
|
279
|
+
for (const entry of childEntries) {
|
|
280
|
+
if (matches.length >= MAX_VISIBLE_TEST_FILES) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (entry.isDirectory) {
|
|
284
|
+
visitDirectory(entry.path, entry.relativePath, depth + 1);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
addMatch(entry.relativePath);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
for (const directoryName of directoryNames) {
|
|
291
|
+
if (matches.length >= MAX_VISIBLE_TEST_FILES) {
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
const directoryPath = join(projectPath, directoryName);
|
|
295
|
+
if (!isDirectory(directoryPath)) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
visitDirectory(directoryPath, directoryName, 0);
|
|
299
|
+
}
|
|
300
|
+
return matches;
|
|
301
|
+
};
|
|
302
|
+
const buildRepoSnapshot = (project) => {
|
|
303
|
+
const topLevelEntries = safeListEntries(project.repo_path);
|
|
304
|
+
const selectedTopLevelEntries = topLevelEntries.slice(0, MAX_TOP_LEVEL_ENTRIES);
|
|
305
|
+
const readmeName = findReadme(topLevelEntries);
|
|
306
|
+
const selectedFiles = [
|
|
307
|
+
...(readmeName ? [readmeName] : []),
|
|
308
|
+
...TOP_LEVEL_TEXT_FILES.filter(fileName => topLevelEntries.includes(fileName)),
|
|
309
|
+
].filter((entry, index, values) => values.indexOf(entry) === index);
|
|
310
|
+
const highValueDirectories = HIGH_VALUE_DIRECTORIES
|
|
311
|
+
.filter(directoryName => isDirectory(join(project.repo_path, directoryName)))
|
|
312
|
+
.slice(0, MAX_HIGH_VALUE_DIRECTORIES);
|
|
313
|
+
const visibleTestFiles = collectVisibleTestFiles(project.repo_path, [
|
|
314
|
+
...highValueDirectories,
|
|
315
|
+
'__tests__',
|
|
316
|
+
'tests',
|
|
317
|
+
]);
|
|
318
|
+
const representativeSourceFiles = selectRepresentativeFiles(project.repo_path, highValueDirectories, isRepresentativeSourceFile, MAX_REPRESENTATIVE_SOURCE_FILES, MAX_REPRESENTATIVE_SOURCE_FILES_PER_DIRECTORY);
|
|
319
|
+
const representativeTestFiles = selectRepresentativeFiles(project.repo_path, [...highValueDirectories, '__tests__', 'tests'], isVisibleTestFile, MAX_REPRESENTATIVE_TEST_FILES, 1);
|
|
320
|
+
const sections = [];
|
|
321
|
+
sections.push('Top-level entries:');
|
|
322
|
+
sections.push(...selectedTopLevelEntries.map(entry => `- ${entry}`));
|
|
323
|
+
if (selectedFiles.length > 0) {
|
|
324
|
+
sections.push('');
|
|
325
|
+
sections.push('Top-level file excerpts:');
|
|
326
|
+
for (const fileName of selectedFiles) {
|
|
327
|
+
const content = safeReadTextFile(join(project.repo_path, fileName));
|
|
328
|
+
if (!content) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
sections.push(`File: ${fileName}`);
|
|
332
|
+
sections.push(content);
|
|
333
|
+
sections.push('');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (highValueDirectories.length > 0) {
|
|
337
|
+
sections.push('');
|
|
338
|
+
sections.push('High-value directory snapshots:');
|
|
339
|
+
const budget = { remaining: MAX_DIRECTORY_SNAPSHOT_LINES };
|
|
340
|
+
for (const directoryName of highValueDirectories) {
|
|
341
|
+
if (budget.remaining <= 0) {
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
const directoryPath = join(project.repo_path, directoryName);
|
|
345
|
+
sections.push(`- ${directoryName}/`);
|
|
346
|
+
budget.remaining -= 1;
|
|
347
|
+
const snapshotLines = [];
|
|
348
|
+
appendDirectorySnapshot(snapshotLines, directoryPath, directoryName, 1, budget);
|
|
349
|
+
sections.push(...(snapshotLines.length > 0 ? snapshotLines : [' - [empty]']));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (representativeSourceFiles.length > 0) {
|
|
353
|
+
sections.push('');
|
|
354
|
+
sections.push('Representative source excerpts:');
|
|
355
|
+
for (const relativePath of representativeSourceFiles) {
|
|
356
|
+
const content = safeReadTextFile(join(project.repo_path, relativePath), MAX_REPRESENTATIVE_FILE_CHARS);
|
|
357
|
+
if (!content) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
sections.push(`File: ${relativePath}`);
|
|
361
|
+
sections.push(content);
|
|
362
|
+
sections.push('');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (representativeTestFiles.length > 0) {
|
|
366
|
+
sections.push('');
|
|
367
|
+
sections.push('Representative test excerpts:');
|
|
368
|
+
for (const relativePath of representativeTestFiles) {
|
|
369
|
+
const content = safeReadTextFile(join(project.repo_path, relativePath), MAX_REPRESENTATIVE_FILE_CHARS);
|
|
370
|
+
if (!content) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
sections.push(`File: ${relativePath}`);
|
|
374
|
+
sections.push(content);
|
|
375
|
+
sections.push('');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (visibleTestFiles.length > 0) {
|
|
379
|
+
sections.push('');
|
|
380
|
+
sections.push('Visible test files:');
|
|
381
|
+
sections.push(...visibleTestFiles.map(filePath => `- ${filePath}`));
|
|
382
|
+
}
|
|
383
|
+
return sections.join('\n').trim();
|
|
384
|
+
};
|
|
385
|
+
const buildScanPrompt = (project, snapshot) => {
|
|
386
|
+
return [
|
|
387
|
+
'You create compact project-memory scan summaries for Archon.',
|
|
388
|
+
'Return strict JSON only. No markdown fences. No explanation.',
|
|
389
|
+
'Use this exact shape:',
|
|
390
|
+
'{"architecture_summary":"string","key_areas":["string"],"important_conventions":["string"],"known_risks_or_unknowns":["string"]}',
|
|
391
|
+
'Rules:',
|
|
392
|
+
'- Keep architecture_summary to 1 short paragraph.',
|
|
393
|
+
'- Keep each list item short and repo-specific.',
|
|
394
|
+
'- Do not invent facts that are not supported by the snapshot.',
|
|
395
|
+
'- Prefer explicit uncertainty in known_risks_or_unknowns over guessing.',
|
|
396
|
+
'- Use known_risks_or_unknowns for concrete repo-specific gaps or risks, not boilerplate missing-visibility disclaimers.',
|
|
397
|
+
'- Avoid generic warnings like "internals are not visible in this snapshot" when representative source excerpts are present.',
|
|
398
|
+
'- important_conventions should describe repo-specific patterns, not generic framework advice.',
|
|
399
|
+
`Project: ${project.name}`,
|
|
400
|
+
`Repo path: ${project.repo_path}`,
|
|
401
|
+
`<repo_snapshot>\n${snapshot}\n</repo_snapshot>`,
|
|
402
|
+
].join('\n\n');
|
|
403
|
+
};
|
|
404
|
+
const isScanOldEnoughForStaleWarning = (updatedAt) => {
|
|
405
|
+
const updatedAtTime = Date.parse(updatedAt);
|
|
406
|
+
if (Number.isNaN(updatedAtTime)) {
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
return Date.now() - updatedAtTime >= STALE_SCAN_WARNING_AGE_MS;
|
|
410
|
+
};
|
|
411
|
+
const extractFirstJsonObject = (value) => {
|
|
412
|
+
const normalized = value.trim();
|
|
413
|
+
if (normalized.startsWith('{') && normalized.endsWith('}')) {
|
|
414
|
+
return JSON.parse(normalized);
|
|
415
|
+
}
|
|
416
|
+
for (let startIndex = 0; startIndex < normalized.length; startIndex += 1) {
|
|
417
|
+
if (normalized[startIndex] !== '{') {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
let depth = 0;
|
|
421
|
+
let inString = false;
|
|
422
|
+
let escaping = false;
|
|
423
|
+
for (let index = startIndex; index < normalized.length; index += 1) {
|
|
424
|
+
const character = normalized[index];
|
|
425
|
+
if (inString) {
|
|
426
|
+
if (escaping) {
|
|
427
|
+
escaping = false;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (character === '\\') {
|
|
431
|
+
escaping = true;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (character === '"') {
|
|
435
|
+
inString = false;
|
|
436
|
+
}
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (character === '"') {
|
|
440
|
+
inString = true;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (character === '{') {
|
|
444
|
+
depth += 1;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (character === '}') {
|
|
448
|
+
depth -= 1;
|
|
449
|
+
if (depth === 0) {
|
|
450
|
+
return JSON.parse(normalized.slice(startIndex, index + 1));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
throw new Error('The scan model did not return valid JSON.');
|
|
456
|
+
};
|
|
457
|
+
const normalizeString = (value) => {
|
|
458
|
+
if (typeof value !== 'string') {
|
|
459
|
+
return '';
|
|
460
|
+
}
|
|
461
|
+
return value.trim();
|
|
462
|
+
};
|
|
463
|
+
const normalizeStringArray = (value) => {
|
|
464
|
+
if (!Array.isArray(value)) {
|
|
465
|
+
return [];
|
|
466
|
+
}
|
|
467
|
+
const normalized = [];
|
|
468
|
+
for (const item of value) {
|
|
469
|
+
if (typeof item !== 'string') {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const trimmed = item.trim();
|
|
473
|
+
if (trimmed && !normalized.includes(trimmed)) {
|
|
474
|
+
normalized.push(trimmed);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return normalized;
|
|
478
|
+
};
|
|
479
|
+
const normalizeScanResult = (value) => {
|
|
480
|
+
const parsed = value && typeof value === 'object' && !Array.isArray(value)
|
|
481
|
+
? value
|
|
482
|
+
: {};
|
|
483
|
+
return {
|
|
484
|
+
architecture_summary: normalizeString(parsed.architecture_summary),
|
|
485
|
+
key_areas: normalizeStringArray(parsed.key_areas),
|
|
486
|
+
important_conventions: normalizeStringArray(parsed.important_conventions),
|
|
487
|
+
known_risks_or_unknowns: normalizeStringArray(parsed.known_risks_or_unknowns),
|
|
488
|
+
};
|
|
489
|
+
};
|
|
490
|
+
const buildSection = (title, items) => {
|
|
491
|
+
if (items.length === 0) {
|
|
492
|
+
return '';
|
|
493
|
+
}
|
|
494
|
+
return `${title}:\n${items.map(item => `- ${item}`).join('\n')}`;
|
|
495
|
+
};
|
|
496
|
+
const buildSummaryMarkdown = (result) => {
|
|
497
|
+
const sections = [
|
|
498
|
+
result.architecture_summary ? `Architecture:\n${result.architecture_summary}` : '',
|
|
499
|
+
buildSection('Key areas', result.key_areas),
|
|
500
|
+
buildSection('Important conventions', result.important_conventions),
|
|
501
|
+
buildSection('Risks and unknowns', result.known_risks_or_unknowns),
|
|
502
|
+
].filter(Boolean);
|
|
503
|
+
return sections.join('\n\n') || null;
|
|
504
|
+
};
|
|
505
|
+
const toArtifactMarkdown = (items) => {
|
|
506
|
+
return items.length > 0 ? items.map(item => `- ${item}`).join('\n') : null;
|
|
507
|
+
};
|
|
508
|
+
const buildArtifacts = (result) => {
|
|
509
|
+
const artifacts = [];
|
|
510
|
+
if (result.architecture_summary) {
|
|
511
|
+
artifacts.push({
|
|
512
|
+
kind: 'architecture',
|
|
513
|
+
title: 'Architecture',
|
|
514
|
+
content_json: JSON.stringify({ summary: result.architecture_summary }),
|
|
515
|
+
content_markdown: result.architecture_summary,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
if (result.key_areas.length > 0) {
|
|
519
|
+
artifacts.push({
|
|
520
|
+
kind: 'key_areas',
|
|
521
|
+
title: 'Key areas',
|
|
522
|
+
content_json: JSON.stringify(result.key_areas),
|
|
523
|
+
content_markdown: toArtifactMarkdown(result.key_areas),
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
if (result.important_conventions.length > 0) {
|
|
527
|
+
artifacts.push({
|
|
528
|
+
kind: 'conventions',
|
|
529
|
+
title: 'Inferred conventions',
|
|
530
|
+
content_json: JSON.stringify(result.important_conventions),
|
|
531
|
+
content_markdown: toArtifactMarkdown(result.important_conventions),
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
if (result.known_risks_or_unknowns.length > 0) {
|
|
535
|
+
artifacts.push({
|
|
536
|
+
kind: 'risks',
|
|
537
|
+
title: 'Risks and unknowns',
|
|
538
|
+
content_json: JSON.stringify(result.known_risks_or_unknowns),
|
|
539
|
+
content_markdown: toArtifactMarkdown(result.known_risks_or_unknowns),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
return artifacts;
|
|
543
|
+
};
|
|
544
|
+
const resolveScanExecution = (project) => {
|
|
545
|
+
if (project.helper_model) {
|
|
546
|
+
return {
|
|
547
|
+
tool: 'opencode',
|
|
548
|
+
model: project.helper_model,
|
|
549
|
+
variant: project.helper_variant,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
tool: config.tool ?? 'opencode',
|
|
554
|
+
model: null,
|
|
555
|
+
variant: null,
|
|
556
|
+
};
|
|
557
|
+
};
|
|
558
|
+
const runGitText = (args, cwd) => {
|
|
559
|
+
return execFileSync('git', args, {
|
|
560
|
+
cwd,
|
|
561
|
+
encoding: 'utf8',
|
|
562
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
563
|
+
}).trim();
|
|
564
|
+
};
|
|
565
|
+
export const getProjectRepoAnchor = (project) => {
|
|
566
|
+
if (!existsSync(project.repo_path)) {
|
|
567
|
+
return { branch: null, head: null };
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
const branchOutput = runGitText(['branch', '--show-current'], project.repo_path);
|
|
571
|
+
const headOutput = runGitText(['rev-parse', 'HEAD'], project.repo_path);
|
|
572
|
+
return {
|
|
573
|
+
branch: branchOutput && branchOutput !== 'HEAD' ? branchOutput : null,
|
|
574
|
+
head: headOutput || null,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
return { branch: null, head: null };
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
const executeProjectContextScan = async (projectId, scanId) => {
|
|
582
|
+
const project = getProjectById(projectId);
|
|
583
|
+
if (!project) {
|
|
584
|
+
throw new Error('Project not found');
|
|
585
|
+
}
|
|
586
|
+
if (!project.memory_enabled) {
|
|
587
|
+
throw new Error('Project memory is disabled for this project');
|
|
588
|
+
}
|
|
589
|
+
const anchor = getProjectRepoAnchor(project);
|
|
590
|
+
updateProjectContextScan(project.id, scanId, {
|
|
591
|
+
status: 'running',
|
|
592
|
+
repo_head: anchor.head,
|
|
593
|
+
repo_branch: anchor.branch,
|
|
594
|
+
error: null,
|
|
595
|
+
});
|
|
596
|
+
const execution = resolveScanExecution(project);
|
|
597
|
+
const snapshot = buildRepoSnapshot(project);
|
|
598
|
+
const prompt = buildScanPrompt(project, snapshot);
|
|
599
|
+
const rawOutput = await runLightweightPrompt(execution, prompt, project.repo_path);
|
|
600
|
+
const result = normalizeScanResult(extractFirstJsonObject(rawOutput));
|
|
601
|
+
const summaryMarkdown = buildSummaryMarkdown(result);
|
|
602
|
+
const artifacts = buildArtifacts(result);
|
|
603
|
+
replaceProjectContextArtifacts(project.id, scanId, artifacts);
|
|
604
|
+
const completedAnchor = getProjectRepoAnchor(project);
|
|
605
|
+
updateProjectContextScan(project.id, scanId, {
|
|
606
|
+
status: 'done',
|
|
607
|
+
repo_head: completedAnchor.head,
|
|
608
|
+
repo_branch: completedAnchor.branch,
|
|
609
|
+
summary_markdown: summaryMarkdown,
|
|
610
|
+
error: null,
|
|
611
|
+
});
|
|
612
|
+
};
|
|
613
|
+
export const enqueueProjectContextScan = (projectId, scanId) => {
|
|
614
|
+
const jobKey = getScanJobKey(projectId);
|
|
615
|
+
if (isRunning(jobKey)) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
enqueue(jobKey, async () => {
|
|
619
|
+
try {
|
|
620
|
+
await executeProjectContextScan(projectId, scanId);
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
624
|
+
updateProjectContextScan(projectId, scanId, {
|
|
625
|
+
status: 'error',
|
|
626
|
+
error: message,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
};
|
|
631
|
+
export const startProjectContextScan = (projectId) => {
|
|
632
|
+
const project = getProjectById(projectId);
|
|
633
|
+
if (!project) {
|
|
634
|
+
return { status: 404, error: 'Not found' };
|
|
635
|
+
}
|
|
636
|
+
if (!project.memory_enabled) {
|
|
637
|
+
return { status: 409, error: 'Project memory is disabled for this project' };
|
|
638
|
+
}
|
|
639
|
+
const activeScan = getActiveProjectContextScan(project.id);
|
|
640
|
+
if (activeScan) {
|
|
641
|
+
return { status: 409, scan: activeScan };
|
|
642
|
+
}
|
|
643
|
+
const execution = resolveScanExecution(project);
|
|
644
|
+
const anchor = getProjectRepoAnchor(project);
|
|
645
|
+
const scan = createProjectContextScan(project.id, {
|
|
646
|
+
repo_head: anchor.head,
|
|
647
|
+
repo_branch: anchor.branch,
|
|
648
|
+
scanner_tool: execution.tool,
|
|
649
|
+
scanner_model: execution.model,
|
|
650
|
+
scanner_variant: execution.variant,
|
|
651
|
+
});
|
|
652
|
+
if (!scan) {
|
|
653
|
+
return { status: 500, error: 'Unable to create the project context scan' };
|
|
654
|
+
}
|
|
655
|
+
enqueueProjectContextScan(project.id, scan.id);
|
|
656
|
+
return { status: 202, scan };
|
|
657
|
+
};
|
|
658
|
+
export const getProjectContextResponse = (projectId) => {
|
|
659
|
+
const project = getProjectById(projectId);
|
|
660
|
+
if (!project) {
|
|
661
|
+
return {
|
|
662
|
+
scan: null,
|
|
663
|
+
stale: false,
|
|
664
|
+
current_repo_head: null,
|
|
665
|
+
current_repo_branch: null,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
const currentAnchor = getProjectRepoAnchor(project);
|
|
669
|
+
const latestScan = getLatestProjectContextScan(project.id);
|
|
670
|
+
const latestSuccessfulScan = getLatestSuccessfulProjectContextScan(project.id);
|
|
671
|
+
return {
|
|
672
|
+
scan: latestScan,
|
|
673
|
+
stale: Boolean(latestSuccessfulScan?.repo_head
|
|
674
|
+
&& currentAnchor.head
|
|
675
|
+
&& latestSuccessfulScan.repo_head !== currentAnchor.head
|
|
676
|
+
&& isScanOldEnoughForStaleWarning(latestSuccessfulScan.updated_at)),
|
|
677
|
+
current_repo_head: currentAnchor.head,
|
|
678
|
+
current_repo_branch: currentAnchor.branch,
|
|
679
|
+
};
|
|
680
|
+
};
|
|
681
|
+
//# sourceMappingURL=projectMemoryScan.js.map
|