codehere 0.4.1 → 0.5.0-alpha.3

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 (24) hide show
  1. package/README.md +64 -7
  2. package/dist/application/use-cases/generate-final-report.js +144 -0
  3. package/dist/index.js +47 -4
  4. package/dist/infrastructure/hooks/claude-sdk-hook.js +18 -3
  5. package/dist/infrastructure/orchestration/aider-adapter.js +12 -2
  6. package/dist/infrastructure/orchestration/claude-code-adapter.js +70 -1
  7. package/dist/infrastructure/orchestration/codex-adapter.js +6 -1
  8. package/dist/infrastructure/orchestration/conductor-adapter.js +12 -2
  9. package/dist/infrastructure/orchestration/gemini-adapter.js +41 -5
  10. package/dist/infrastructure/orchestration/generic-cli-adapter.js +10 -2
  11. package/dist/infrastructure/orchestration/memory-injector.js +337 -0
  12. package/dist/infrastructure/orchestration/opencode-adapter.js +34 -4
  13. package/dist/infrastructure/orchestration/progress-aggregator.js +119 -0
  14. package/dist/infrastructure/orchestration/readiness.js +2 -5
  15. package/dist/infrastructure/orchestration/session-manager.js +65 -2
  16. package/dist/infrastructure/orchestration/session-persistence.js +13 -1
  17. package/dist/infrastructure/protocols/mcp-server-impl.js +86 -14
  18. package/dist/infrastructure/protocols/ui-page.js +665 -67
  19. package/dist/presentation/cli/commands/init-command.js +345 -24
  20. package/dist/presentation/cli/commands/memory-command.js +1017 -0
  21. package/dist/presentation/cli/commands/protocols-builder.js +254 -0
  22. package/dist/presentation/cli/commands/protocols-command.js +84 -0
  23. package/dist/presentation/cli/commands/repo-map-builder.js +247 -0
  24. package/package.json +4 -3
@@ -0,0 +1,254 @@
1
+ /**
2
+ * F3 Task 7 — protocols/hook_patterns.json exporter.
3
+ *
4
+ * Reads the trust-scorer's CODE_PATTERNS bank (the in-code TypeScript
5
+ * source of truth for the safety hook's pattern matchers) and exports it
6
+ * to .codehere/protocols/hook_patterns.json so the patterns are:
7
+ *
8
+ * - inspectable (auditors, security reviewers can read the JSON)
9
+ * - version-controllable (the file lives in the repo)
10
+ * - shareable (cross-tool: e.g. PR our patterns into the
11
+ * agentic-stack ecosystem per ADR-006)
12
+ * - extensible (project-specific patterns under `patterns_project[]`
13
+ * survive re-exports)
14
+ *
15
+ * Schema split (regenerable / preserved):
16
+ * patterns_builtin[] — regenerated from CODE_PATTERNS on each export
17
+ * patterns_project[] — user-edited; preserved across re-exports
18
+ *
19
+ * Each pattern is identified by a stable `id` slugified from category +
20
+ * description. Same code → same id, so users can reference patterns
21
+ * deterministically (e.g. "ignore the `secrets-hardcoded-credential`
22
+ * pattern in this folder").
23
+ *
24
+ * Forward-compatibility: regex is serialized as a JSON-portable string
25
+ * (no special characters that would break JSON parsing); the flags are
26
+ * a separate field. This avoids the JSON-vs-RegExp impedance mismatch.
27
+ */
28
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
29
+ import { join, dirname } from 'path';
30
+ import { CODE_PATTERNS } from '../../../infrastructure/security/trust-scorer.js';
31
+ /**
32
+ * Slugify category + description into a stable id.
33
+ *
34
+ * "Secrets" + "Hardcoded credential" → "secrets-hardcoded-credential"
35
+ * "SQL Injection" + "String interpolation in SQL query" → "sql-injection-string-interpolation-in-sql-query"
36
+ *
37
+ * Stability matters: re-exports must produce the same id from the same
38
+ * source. Cite this slug in test fixtures, downstream tools, project
39
+ * overrides etc.
40
+ */
41
+ function slugifyPatternId(category, description) {
42
+ return (category + ' ' + description)
43
+ .toLowerCase()
44
+ .replace(/[^a-z0-9]+/g, '-')
45
+ .replace(/^-+|-+$/g, '');
46
+ }
47
+ /**
48
+ * Serialize a JS RegExp to a JSON-portable {regex, flags} pair.
49
+ * `regex` is the source string (without surrounding `/`); `flags` is the
50
+ * flags string ('gi' etc.).
51
+ */
52
+ function serializeRegex(re) {
53
+ return { regex: re.source, flags: re.flags };
54
+ }
55
+ /**
56
+ * Assign stable, source-order-independent ids to a list of patterns.
57
+ *
58
+ * The sort key is (category, description, regex.source) — a 3-tier
59
+ * comparator. The first two tiers come from K2 Round 1 (sort by
60
+ * category + description so duplicates get deterministic suffix
61
+ * assignment). The 3rd tier (regex source) is the codex Round 2 fix:
62
+ * (category, description) alone leaves stable-sort fall-through to
63
+ * source order when two patterns share both fields, which means
64
+ * reordering CODE_PATTERNS would still flip which one gets the base id
65
+ * vs `-2`. Adding regex.source disambiguates definitively — two patterns
66
+ * with identical category + description + regex are the SAME pattern;
67
+ * they can't both exist.
68
+ *
69
+ * Returns a new sorted array with `id` attached. Pure; safe to test
70
+ * with arbitrary fixtures.
71
+ */
72
+ export function assignStableIds(patterns) {
73
+ const sorted = [...patterns].sort((a, b) => {
74
+ const c = a.category.localeCompare(b.category);
75
+ if (c !== 0)
76
+ return c;
77
+ const d = a.description.localeCompare(b.description);
78
+ if (d !== 0)
79
+ return d;
80
+ return a.regex.source.localeCompare(b.regex.source);
81
+ });
82
+ const seenIds = new Map();
83
+ return sorted.map((p) => {
84
+ const baseId = slugifyPatternId(p.category, p.description);
85
+ const seen = seenIds.get(baseId) ?? 0;
86
+ seenIds.set(baseId, seen + 1);
87
+ const id = seen === 0 ? baseId : `${baseId}-${seen + 1}`;
88
+ return { ...p, id };
89
+ });
90
+ }
91
+ /**
92
+ * Build the export object from CODE_PATTERNS. Pure — does not touch disk.
93
+ *
94
+ * Delegates id assignment to `assignStableIds` so the source-order-
95
+ * independence property has a direct unit test (see protocols-export.test.ts).
96
+ */
97
+ export function buildHookPatternsExport() {
98
+ const withIds = assignStableIds(CODE_PATTERNS);
99
+ const builtin = withIds.map((p) => {
100
+ const { regex, flags } = serializeRegex(p.regex);
101
+ const out = {
102
+ id: p.id,
103
+ regex,
104
+ regex_flags: flags,
105
+ severity: p.severity,
106
+ category: p.category,
107
+ description: p.description,
108
+ };
109
+ if (p.cwe)
110
+ out.cwe = p.cwe;
111
+ if (p.scanRaw)
112
+ out.scanRaw = p.scanRaw;
113
+ if (p.requiresContext) {
114
+ const ctx = serializeRegex(p.requiresContext);
115
+ out.requiresContext = { regex: ctx.regex, flags: ctx.flags };
116
+ }
117
+ return out;
118
+ });
119
+ return {
120
+ version: 'v0.5',
121
+ generatedAt: new Date().toISOString(),
122
+ source: 'agent/src/infrastructure/security/trust-scorer.ts',
123
+ _note: "Exported from codehere's trust-scorer pattern bank via 'codehere protocols export'. " +
124
+ "DO NOT hand-edit `patterns_builtin[]` — it's regenerated on every export. " +
125
+ "Add project-specific patterns to `patterns_project[]` — those are preserved across re-exports. " +
126
+ "Pattern ids are stable slugs of category + description; reference them deterministically.",
127
+ patterns_builtin: builtin,
128
+ patterns_project: [],
129
+ };
130
+ }
131
+ /**
132
+ * Detect whether the on-disk file is the init-time placeholder.
133
+ *
134
+ * Two placeholder shapes are recognized:
135
+ *
136
+ * 1. POST-2026-04-28 seed (current init) — split schema, empty arrays:
137
+ * { version, patterns_builtin: [], patterns_project: [], _note }
138
+ *
139
+ * 2. PRE-2026-04-28 seed (legacy init) — single empty `patterns` array:
140
+ * { version, patterns: [], _note }
141
+ *
142
+ * Either one is safe to overwrite without --force. A NON-empty legacy
143
+ * `patterns` array is NOT a placeholder — see `extractLegacyProjectPatterns`
144
+ * below which migrates user content into `patterns_project[]` rather than
145
+ * silently discarding it (codex Round 1 fix — the legacy seed's _note
146
+ * explicitly told users to add patterns to the legacy field, so users may
147
+ * have followed that guidance).
148
+ */
149
+ function isInitPlaceholder(parsed) {
150
+ if (typeof parsed !== 'object' || parsed === null)
151
+ return false;
152
+ const obj = parsed;
153
+ // Current shape: split schema, both arrays empty.
154
+ if (Array.isArray(obj.patterns_builtin) &&
155
+ obj.patterns_builtin.length === 0 &&
156
+ Array.isArray(obj.patterns_project) &&
157
+ obj.patterns_project.length === 0) {
158
+ return true;
159
+ }
160
+ // Legacy shape: single empty `patterns` array, no _builtin/_project.
161
+ if (Array.isArray(obj.patterns) &&
162
+ obj.patterns.length === 0 &&
163
+ obj.patterns_builtin === undefined &&
164
+ obj.patterns_project === undefined) {
165
+ return true;
166
+ }
167
+ return false;
168
+ }
169
+ /**
170
+ * Extract user-added patterns from the LEGACY `patterns[]` field, if any.
171
+ * Used to migrate non-empty legacy files into `patterns_project[]` so users
172
+ * who followed the legacy seed's _note advice ("add patterns to `patterns`")
173
+ * don't lose their work on first export (codex Round 1 fix).
174
+ *
175
+ * Returns `null` if the file isn't a legacy-shape file with non-empty
176
+ * patterns; callers fall through to the normal --rebuild path which reads
177
+ * `patterns_project[]` directly.
178
+ */
179
+ function extractLegacyProjectPatterns(parsed) {
180
+ if (typeof parsed !== 'object' || parsed === null)
181
+ return null;
182
+ const obj = parsed;
183
+ if (Array.isArray(obj.patterns) &&
184
+ obj.patterns.length > 0 &&
185
+ obj.patterns_builtin === undefined &&
186
+ obj.patterns_project === undefined) {
187
+ return obj.patterns;
188
+ }
189
+ return null;
190
+ }
191
+ /**
192
+ * Disk-side wrapper: build + write to .codehere/protocols/hook_patterns.json.
193
+ *
194
+ * If the existing file is the init-time placeholder, overwrite without
195
+ * requiring --rebuild. Otherwise:
196
+ * - existing file present + no --rebuild → 'exists' (refuse)
197
+ * - existing file present + --rebuild → preserve patterns_project[],
198
+ * regenerate patterns_builtin[]
199
+ * - no existing file → write fresh
200
+ */
201
+ export function writeHookPatternsExport(codehereDir, options = {}) {
202
+ const targetPath = join(codehereDir, 'protocols/hook_patterns.json');
203
+ let preservedProject = [];
204
+ if (existsSync(targetPath)) {
205
+ let parsed;
206
+ try {
207
+ parsed = JSON.parse(readFileSync(targetPath, 'utf8'));
208
+ }
209
+ catch {
210
+ return { result: 'failed', path: targetPath, error: 'existing file is not valid JSON' };
211
+ }
212
+ // Codex Round 1 fix: migrate non-empty legacy `patterns[]` content into
213
+ // `patterns_project[]` BEFORE the placeholder/rebuild branches. The
214
+ // legacy init seed's _note told users to add patterns to the legacy
215
+ // field; a fresh export must preserve any content they added rather
216
+ // than silently discard it.
217
+ const legacy = extractLegacyProjectPatterns(parsed);
218
+ if (legacy !== null) {
219
+ preservedProject = legacy;
220
+ // Fall through to writing — we treat a non-empty-legacy file as
221
+ // overwriteable (the legacy patterns now live in preservedProject).
222
+ }
223
+ else if (isInitPlaceholder(parsed)) {
224
+ // Empty placeholder (either schema shape) — overwrite without --rebuild.
225
+ }
226
+ else if (!options.rebuild) {
227
+ return { result: 'exists', path: targetPath };
228
+ }
229
+ else {
230
+ // --rebuild + non-placeholder, non-legacy existing file → preserve
231
+ // patterns_project[].
232
+ const obj = parsed;
233
+ if (Array.isArray(obj.patterns_project)) {
234
+ preservedProject = obj.patterns_project;
235
+ }
236
+ }
237
+ }
238
+ const built = buildHookPatternsExport();
239
+ built.patterns_project = preservedProject;
240
+ try {
241
+ mkdirSync(dirname(targetPath), { recursive: true });
242
+ writeFileSync(targetPath, JSON.stringify(built, null, 2) + '\n');
243
+ }
244
+ catch (e) {
245
+ return { result: 'failed', path: targetPath, error: e instanceof Error ? e.message : String(e) };
246
+ }
247
+ return {
248
+ result: 'exported',
249
+ path: targetPath,
250
+ builtinCount: built.patterns_builtin.length,
251
+ projectCount: built.patterns_project.length,
252
+ };
253
+ }
254
+ //# sourceMappingURL=protocols-builder.js.map
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Protocols Command — F3 v0.5 protocols layer operations.
3
+ *
4
+ * Today: `codehere protocols export` writes the trust-scorer's pattern
5
+ * bank to .codehere/protocols/hook_patterns.json. Future: `protocols
6
+ * import`, `protocols verify`, `protocols diff` etc.
7
+ *
8
+ * The dispatch shape mirrors `memory-command.ts` so future actions slot
9
+ * in without touching index.ts beyond a single .option() addition.
10
+ */
11
+ import { existsSync } from 'fs';
12
+ import { join } from 'path';
13
+ import { colors, success, info, error, newline } from '../../../ui.js';
14
+ import { findCodebaseRoot } from '../../../domain/services/codebase-detector.js';
15
+ import { writeHookPatternsExport } from './protocols-builder.js';
16
+ function executeProtocolsExport(options) {
17
+ const codeInfo = findCodebaseRoot();
18
+ const root = codeInfo?.rootPath ?? process.cwd();
19
+ const codehereDir = join(root, '.codehere');
20
+ newline();
21
+ console.log(colors.bold('codehere protocols export'));
22
+ console.log(colors.dim(` exporting trust-scorer pattern bank → ${codehereDir}/protocols/hook_patterns.json`));
23
+ newline();
24
+ if (!existsSync(codehereDir)) {
25
+ console.log(` ${error('refused')} .codehere/ not found at this root.`);
26
+ console.log(` ${colors.dim('Run')} ${colors.cyan('codehere init')} ${colors.dim('first.')}`);
27
+ newline();
28
+ process.exitCode = 1;
29
+ return;
30
+ }
31
+ const result = writeHookPatternsExport(codehereDir, { rebuild: !!options.rebuild });
32
+ switch (result.result) {
33
+ case 'exported': {
34
+ const projectNote = result.projectCount && result.projectCount > 0
35
+ ? ` · ${result.projectCount} project pattern${result.projectCount === 1 ? '' : 's'} preserved`
36
+ : '';
37
+ console.log(` ${success('exported')} ${result.path} ${colors.dim(`(${result.builtinCount} built-in patterns${projectNote})`)}`);
38
+ break;
39
+ }
40
+ case 'exists':
41
+ console.log(` ${info('exists ')} ${result.path} ${colors.dim('(use --rebuild to refresh built-in patterns; project patterns are preserved)')}`);
42
+ break;
43
+ case 'failed':
44
+ console.log(` ${error('failed ')} ${result.path} ${colors.dim(`(${result.error})`)}`);
45
+ process.exitCode = 1;
46
+ break;
47
+ }
48
+ newline();
49
+ console.log(colors.bold('Next:'));
50
+ console.log(` - Inspect ${colors.cyan('.codehere/protocols/hook_patterns.json')} to see what the safety hook matches`);
51
+ console.log(` - Add project-specific patterns under ${colors.cyan('patterns_project[]')} — they survive re-exports`);
52
+ console.log(` ${colors.dim('(Edit patterns_builtin[] is a no-op — re-exports overwrite it from trust-scorer.ts.)')}`);
53
+ newline();
54
+ }
55
+ export function executeProtocolsCommand(action, options = {}) {
56
+ try {
57
+ switch (action) {
58
+ case 'export':
59
+ executeProtocolsExport(options);
60
+ return;
61
+ case undefined:
62
+ case 'help':
63
+ console.log(`
64
+ codehere protocols <action> — F3 protocols layer operations
65
+
66
+ export Export trust-scorer pattern bank to .codehere/protocols/hook_patterns.json
67
+
68
+ Coming later:
69
+ import Apply patterns from a file (cross-tool ingestion)
70
+ verify Check that the on-disk file matches the in-code pattern bank
71
+ `);
72
+ return;
73
+ default:
74
+ console.error(`\n ${error('failed')} unknown protocols action: ${action}\n Try: codehere protocols help\n`);
75
+ process.exitCode = 1;
76
+ }
77
+ }
78
+ catch (e) {
79
+ const msg = e instanceof Error ? e.message : String(e);
80
+ console.error(`\n ${error('failed')} codehere protocols ${action ?? ''} — ${msg}\n`);
81
+ process.exitCode = 1;
82
+ }
83
+ }
84
+ //# sourceMappingURL=protocols-command.js.map
@@ -0,0 +1,247 @@
1
+ /**
2
+ * F3 Task 4 — repo_map.json builder (v0.5 TS-native MVP).
3
+ *
4
+ * v0.5 SCOPE:
5
+ * - Walks `git ls-files` (respects .gitignore via git itself).
6
+ * - For .ts/.tsx/.js/.jsx/.py files, regex-extracts top-level public-surface
7
+ * symbols (export function/class/const/interface/type, def/class for Python).
8
+ * - Reference count via `git grep -c` per symbol name.
9
+ * - Writes JSON to .codehere/memory/episodic/repo_map.json.
10
+ *
11
+ * v0.6 PLAN:
12
+ * - Replace the regex extractor with proper tree-sitter parsing
13
+ * (Aider's repomap.py MIT port — Python helper + TS shell-out, OR
14
+ * a TS-native tree-sitter library evaluation).
15
+ * - Add PageRank-style symbol importance (currently raw reference count).
16
+ * - Incremental rebuild via the per-file `hash` field already in the schema.
17
+ *
18
+ * Schema is forward-compatible — only the extractor implementation changes.
19
+ *
20
+ * Why regex MVP for v0.5:
21
+ * - Zero new deps (tree-sitter would mean Python + 5 packages OR a 30+ MB
22
+ * TS dep). codehere is bootstrap-zero-cost; every dep is friction.
23
+ * - Closes the F3 episodic/repo_map.json layer enough for spawned agents
24
+ * to read it as a starting context block.
25
+ * - Honest about limitations — see schema._note in the output JSON.
26
+ */
27
+ import { spawnSync } from 'child_process';
28
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs';
29
+ import { createHash } from 'crypto';
30
+ import { join, dirname, extname } from 'path';
31
+ const SUPPORTED_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py']);
32
+ /**
33
+ * Per-language extractors. v0.5 regex-based — line-anchored to avoid
34
+ * matching identifiers inside string literals. Returns 1-indexed line numbers.
35
+ *
36
+ * v0.6 will swap these for AST-based extractors. The interface stays the
37
+ * same: (body) → RepoMapSymbol[].
38
+ */
39
+ function extractTsLikeSymbols(body) {
40
+ const lines = body.split('\n');
41
+ const symbols = [];
42
+ // export function NAME( | export async function NAME(
43
+ // export class NAME
44
+ // export const NAME = | export let NAME = | export var NAME =
45
+ // export interface NAME { | export type NAME =
46
+ const PATTERNS = [
47
+ [/^export\s+(?:async\s+)?function\s+(\w+)\s*[<(]/, 'function'],
48
+ [/^export\s+(?:abstract\s+)?class\s+(\w+)/, 'class'],
49
+ [/^export\s+(?:const|let|var)\s+(\w+)\s*[:=]/, 'const'],
50
+ [/^export\s+interface\s+(\w+)/, 'interface'],
51
+ [/^export\s+type\s+(\w+)\s*=/, 'type'],
52
+ [/^export\s+enum\s+(\w+)/, 'enum'],
53
+ ];
54
+ for (let i = 0; i < lines.length; i++) {
55
+ for (const [re, kind] of PATTERNS) {
56
+ const m = lines[i].match(re);
57
+ if (m) {
58
+ symbols.push({ kind, name: m[1], line: i + 1 });
59
+ break;
60
+ }
61
+ }
62
+ }
63
+ return symbols;
64
+ }
65
+ function extractPythonSymbols(body) {
66
+ const lines = body.split('\n');
67
+ const symbols = [];
68
+ // Top-level (column 0) def NAME( | class NAME( | class NAME:
69
+ const PATTERNS = [
70
+ [/^def\s+(\w+)\s*\(/, 'def'],
71
+ [/^class\s+(\w+)\s*[(:]/, 'class'],
72
+ ];
73
+ for (let i = 0; i < lines.length; i++) {
74
+ for (const [re, kind] of PATTERNS) {
75
+ const m = lines[i].match(re);
76
+ if (m) {
77
+ symbols.push({ kind, name: m[1], line: i + 1 });
78
+ break;
79
+ }
80
+ }
81
+ }
82
+ return symbols;
83
+ }
84
+ function extractSymbols(filePath, body) {
85
+ const ext = extname(filePath);
86
+ if (ext === '.py')
87
+ return extractPythonSymbols(body);
88
+ if (SUPPORTED_EXTENSIONS.has(ext))
89
+ return extractTsLikeSymbols(body);
90
+ return [];
91
+ }
92
+ /**
93
+ * Hash file content for incremental-rebuild support in v0.6.
94
+ * Format: `sha256:<64-hex>` so the algorithm is self-describing.
95
+ */
96
+ function hashContent(body) {
97
+ return 'sha256:' + createHash('sha256').update(body).digest('hex');
98
+ }
99
+ /**
100
+ * `git ls-files` returns paths relative to the repo root. Filters out
101
+ * non-supported extensions in one pass. If `git` isn't a repo, returns null
102
+ * so the caller can refuse with a clear message.
103
+ */
104
+ function gitListFiles(repoRoot) {
105
+ const r = spawnSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf-8' });
106
+ if (r.status !== 0)
107
+ return null;
108
+ return r.stdout
109
+ .split('\0')
110
+ .filter(p => p.length > 0)
111
+ .filter(p => SUPPORTED_EXTENSIONS.has(extname(p)));
112
+ }
113
+ /**
114
+ * Count references to a symbol name across the repo via git grep.
115
+ * `-c` returns "<file>:<count>" per matching file; sum the counts.
116
+ *
117
+ * Uses word-boundary regex so `popular` doesn't match `unpopular`. The grep
118
+ * finds DEFINITIONS too, so subtract the definition count to get pure refs.
119
+ */
120
+ function countReferences(repoRoot, symbolName) {
121
+ // -F = fixed string, -w = word boundary. Combined with grep itself.
122
+ const r = spawnSync('git', ['grep', '-c', '-w', '-F', '--', symbolName], { cwd: repoRoot, encoding: 'utf-8' });
123
+ if (r.status !== 0 && r.status !== 1)
124
+ return 0;
125
+ let total = 0;
126
+ for (const line of r.stdout.split('\n')) {
127
+ const m = line.match(/:(\d+)$/);
128
+ if (m)
129
+ total += parseInt(m[1], 10);
130
+ }
131
+ // Subtract 1 for the definition line itself (best-effort — undercounts if
132
+ // the symbol is defined in multiple files; v0.6 with AST will be accurate).
133
+ return Math.max(0, total - 1);
134
+ }
135
+ /**
136
+ * Build the repo_map for the repo at `repoRoot`. Returns the JSON object —
137
+ * caller writes to disk. Pure-ish (still spawns git), no codehere-specific
138
+ * paths inside.
139
+ *
140
+ * The `_stats` block in the returned RepoMap persists to disk so downstream
141
+ * agents know whether the map is incomplete (e.g. >1MB files skipped during
142
+ * walk).
143
+ */
144
+ export function buildRepoMap(repoRoot) {
145
+ if (!existsSync(join(repoRoot, '.git'))) {
146
+ return { error: 'not a git repo (run `git init` first)' };
147
+ }
148
+ const files = gitListFiles(repoRoot);
149
+ if (files === null) {
150
+ return { error: 'git ls-files failed' };
151
+ }
152
+ const fileMap = {};
153
+ const allSymbols = [];
154
+ let skippedLargeFiles = 0;
155
+ for (const rel of files) {
156
+ const abs = join(repoRoot, rel);
157
+ // Skip giant files (>1MB) — surfaced in the result so users see the count.
158
+ try {
159
+ if (statSync(abs).size > 1_000_000) {
160
+ skippedLargeFiles++;
161
+ continue;
162
+ }
163
+ }
164
+ catch {
165
+ continue;
166
+ }
167
+ let body;
168
+ try {
169
+ body = readFileSync(abs, 'utf-8');
170
+ }
171
+ catch {
172
+ // File deleted between ls-files and read — skip.
173
+ continue;
174
+ }
175
+ const symbols = extractSymbols(rel, body);
176
+ fileMap[rel] = {
177
+ hash: hashContent(body),
178
+ symbols,
179
+ };
180
+ for (const s of symbols) {
181
+ allSymbols.push({ name: s.name, file: rel, line: s.line });
182
+ }
183
+ }
184
+ // Rank by reference count.
185
+ //
186
+ // codex Round 1 fix: the previous version applied .slice(0, 200) BEFORE
187
+ // grepping, which meant "first 200 discovered names" not "top 200
188
+ // candidates." Symbols defined in files traversed late silently got
189
+ // references=0 and dropped out of topSymbols, regardless of how heavily
190
+ // they were referenced. Now we grep ALL unique names. O(N) git greps is
191
+ // acceptable for v0.5 — codehere's own 249-symbol repo runs in ~5s.
192
+ // v0.6's tree-sitter port will swap raw grep for AST-aware reference
193
+ // resolution + PageRank, which is faster AND more accurate.
194
+ const uniqueNames = Array.from(new Set(allSymbols.map(s => s.name)));
195
+ const refCounts = new Map();
196
+ for (const name of uniqueNames) {
197
+ refCounts.set(name, countReferences(repoRoot, name));
198
+ }
199
+ const topSymbols = allSymbols
200
+ .map(s => ({ ...s, references: refCounts.get(s.name) ?? 0 }))
201
+ // Stable sort: references DESC, then name ASC for tie-breaking.
202
+ .sort((a, b) => b.references - a.references || a.name.localeCompare(b.name))
203
+ .slice(0, 50);
204
+ return {
205
+ version: 'v0.5',
206
+ generatedAt: new Date().toISOString(),
207
+ rootPath: repoRoot,
208
+ _note: 'TS-native regex MVP — KNOWN MISSES (v0.5, addressed in v0.6 tree-sitter port): ' +
209
+ '`export default function/class` (no name on the export line), ' +
210
+ '`export { foo, bar }` blocks, `export { foo } from "..."` re-exports, ' +
211
+ '`export * from "..."` star re-exports, multi-line `export { ... }` blocks, ' +
212
+ '`export declare`, CommonJS `module.exports` / `exports.X` assignments, ' +
213
+ 'symbols not at column 0 in Python. Reference counts via word-bounded ' +
214
+ 'git grep undercount when a name is defined in multiple files. ' +
215
+ 'Read `_stats.skippedLargeFiles` to detect files left out of the walk.',
216
+ _stats: { skippedLargeFiles },
217
+ files: fileMap,
218
+ topSymbols,
219
+ };
220
+ }
221
+ /**
222
+ * Disk-side wrapper: build + write to .codehere/memory/episodic/repo_map.json.
223
+ *
224
+ * `_stats` is preserved in the on-disk JSON (codex Round 2 fix) so spawned
225
+ * agents reading the map can tell "no symbols in this file" from "this file
226
+ * was skipped during the walk." The CLI also surfaces it on the return value.
227
+ */
228
+ export function writeRepoMap(repoRoot, codehereDir, options = {}) {
229
+ const targetPath = join(codehereDir, 'memory/episodic/repo_map.json');
230
+ if (existsSync(targetPath) && !options.rebuild) {
231
+ return { result: 'exists', path: targetPath };
232
+ }
233
+ const built = buildRepoMap(repoRoot);
234
+ if ('error' in built) {
235
+ return { result: 'failed', path: targetPath, error: built.error };
236
+ }
237
+ mkdirSync(dirname(targetPath), { recursive: true });
238
+ writeFileSync(targetPath, JSON.stringify(built, null, 2) + '\n');
239
+ return {
240
+ result: 'written',
241
+ path: targetPath,
242
+ fileCount: Object.keys(built.files).length,
243
+ symbolCount: Object.values(built.files).reduce((acc, f) => acc + f.symbols.length, 0),
244
+ skippedLargeFiles: built._stats.skippedLargeFiles,
245
+ };
246
+ }
247
+ //# sourceMappingURL=repo-map-builder.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codehere",
3
- "version": "0.4.1",
4
- "description": "A PM for your AI coding agents. Delegate, orchestrate, and audit Claude Code, Codex, Aider, and OpenCode from one local web UI — every AI action traced, every file scored.",
3
+ "version": "0.5.0-alpha.3",
4
+ "description": "A PM for your AI coding agents. Delegate, orchestrate, and audit Claude Code, Codex, Aider, OpenCode, Gemini, and any user-configured CLI agent from one local web UI — every AI action traced, every file scored, every spawn warmed with project memory.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
7
7
  "bin": {
@@ -29,7 +29,8 @@
29
29
  "lint": "eslint src/ --ext .ts",
30
30
  "format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'",
31
31
  "format:check": "prettier --check 'src/**/*.ts' 'test/**/*.ts'",
32
- "sbom": "npm sbom --sbom-format=spdx --omit=dev"
32
+ "sbom": "npm sbom --sbom-format=spdx --omit=dev",
33
+ "pretag:check": "../scripts/pretag-npm-check.sh"
33
34
  },
34
35
  "keywords": [
35
36
  "ai-coding-agents",