codehere 0.8.0 → 1.0.2

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 (119) hide show
  1. package/README.md +37 -184
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +329 -0
  5. package/dist/index.js +74 -1004
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
  7. package/dist/presentation/cli/commands/check-command.js +251 -0
  8. package/dist/presentation/cli/commands/help-command.js +22 -286
  9. package/dist/ui.js +19 -438
  10. package/dist/utils/version.js +0 -12
  11. package/package.json +18 -27
  12. package/dist/application/use-cases/domain-trust.js +0 -110
  13. package/dist/application/use-cases/generate-final-report.js +0 -134
  14. package/dist/application/use-cases/generate-narration.js +0 -128
  15. package/dist/application/use-cases/generate-thread-title.js +0 -178
  16. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  17. package/dist/application/use-cases/thread-export.js +0 -155
  18. package/dist/application/use-cases/verify-claim.js +0 -274
  19. package/dist/application/use-cases/write-pass.js +0 -277
  20. package/dist/domain/entities/task.js +0 -61
  21. package/dist/domain/services/codebase-detector.js +0 -91
  22. package/dist/error-handler.js +0 -251
  23. package/dist/infrastructure/completion/bash-completion.js +0 -25
  24. package/dist/infrastructure/completion/fish-completion.js +0 -22
  25. package/dist/infrastructure/completion/zsh-completion.js +0 -30
  26. package/dist/infrastructure/config/config-manager.js +0 -173
  27. package/dist/infrastructure/config/product-context.js +0 -67
  28. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  29. package/dist/infrastructure/context/adapters/figma-source.js +0 -163
  30. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
  31. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  32. package/dist/infrastructure/context/context-resolver.js +0 -144
  33. package/dist/infrastructure/context/context-source.js +0 -114
  34. package/dist/infrastructure/context/oauth-flow.js +0 -207
  35. package/dist/infrastructure/context/oauth-store.js +0 -119
  36. package/dist/infrastructure/context/registry-bootstrap.js +0 -99
  37. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  38. package/dist/infrastructure/cost/budget-config.js +0 -113
  39. package/dist/infrastructure/cost/cost-router.js +0 -104
  40. package/dist/infrastructure/cost/spend-aggregator.js +0 -147
  41. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
  42. package/dist/infrastructure/hooks/hook-mode.js +0 -71
  43. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
  44. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  45. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  46. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  47. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  48. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  49. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
  50. package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
  51. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  52. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
  53. package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
  54. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
  55. package/dist/infrastructure/orchestration/context-handoff.js +0 -144
  56. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  57. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  58. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  59. package/dist/infrastructure/orchestration/failover-config.js +0 -77
  60. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  61. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
  62. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
  63. package/dist/infrastructure/orchestration/graphify-context.js +0 -254
  64. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  65. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
  66. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  67. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  68. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  69. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
  70. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
  71. package/dist/infrastructure/orchestration/readiness.js +0 -103
  72. package/dist/infrastructure/orchestration/session-manager.js +0 -1091
  73. package/dist/infrastructure/orchestration/session-persistence.js +0 -258
  74. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  75. package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
  76. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  77. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  78. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
  79. package/dist/infrastructure/protocols/probe-port.js +0 -36
  80. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  81. package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
  82. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  83. package/dist/infrastructure/protocols/verify-page.js +0 -983
  84. package/dist/infrastructure/security/file-collection.js +0 -118
  85. package/dist/infrastructure/security/input-validator.js +0 -152
  86. package/dist/infrastructure/security/permissions-config.js +0 -234
  87. package/dist/infrastructure/security/trust-scorer.js +0 -657
  88. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  89. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  90. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  91. package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
  92. package/dist/infrastructure/storage/project-thread-reader.js +0 -172
  93. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  94. package/dist/infrastructure/storage/task-repository.js +0 -206
  95. package/dist/monitoring.js +0 -212
  96. package/dist/presentation/cli/commands/audit-command.js +0 -126
  97. package/dist/presentation/cli/commands/budget-command.js +0 -79
  98. package/dist/presentation/cli/commands/config-command.js +0 -125
  99. package/dist/presentation/cli/commands/context-source-command.js +0 -198
  100. package/dist/presentation/cli/commands/hook-command.js +0 -265
  101. package/dist/presentation/cli/commands/init-command.js +0 -578
  102. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  103. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  104. package/dist/presentation/cli/commands/permission-command.js +0 -116
  105. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  106. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  107. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  108. package/dist/presentation/cli/commands/report-command.js +0 -387
  109. package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
  110. package/dist/presentation/cli/commands/setup-command.js +0 -307
  111. package/dist/presentation/cli/commands/status-command.js +0 -661
  112. package/dist/presentation/cli/commands/thread-command.js +0 -288
  113. package/dist/presentation/cli/commands/trust-command.js +0 -306
  114. package/dist/presentation/cli/commands/verify-command.js +0 -233
  115. package/dist/presentation/cli/error-display.js +0 -336
  116. package/dist/presentation/cli/repl.js +0 -797
  117. package/dist/presentation/cli/task-deliverable.js +0 -127
  118. package/dist/testing/cassette.js +0 -125
  119. package/dist/testing/replay-adapter.js +0 -166
@@ -1,254 +0,0 @@
1
- /**
2
- * graphify-context — shared structural memory layer (P1).
3
- *
4
- * Wraps `graphify-ts` CLI to compile compact context packs for agent prompts.
5
- * Every spawn can optionally get a graphify context pack that replaces
6
- * raw file reads + unstructured JSONL scans.
7
- *
8
- * All functions are synchronous (execFileSync) to slot into the sync
9
- * spawn() call chain without breaking the API.
10
- *
11
- * P1 integration points:
12
- * - Session spawn: inject context pack into prompt prefix
13
- * - Failover handoff (P2): replacement agent inherits the same pack
14
- * - S7 compaction (future): use graph to prune stale session logs
15
- * - PM-brain routing (future): community_overview for agent selection
16
- */
17
- import { execFileSync } from 'child_process';
18
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
19
- import { join } from 'path';
20
- import { randomUUID } from 'crypto';
21
- // ── Constants ──────────────────────────────────────────────────────────────────
22
- const GRAPHIFY_OUT = 'graphify-out';
23
- const GRAPH_JSON = 'graph.json';
24
- const CONTEXT_PACKS_DIR = '.codehere';
25
- const CONTEXT_PACKS_FILE = 'context-packs.jsonl';
26
- // ── Helpers ────────────────────────────────────────────────────────────────────
27
- function getGraphPath(cwd) {
28
- return join(cwd, GRAPHIFY_OUT, GRAPH_JSON);
29
- }
30
- function getGraphStats(cwd) {
31
- const graphPath = getGraphPath(cwd);
32
- try {
33
- if (!existsSync(graphPath)) {
34
- return { nodeCount: 0, edgeCount: 0 };
35
- }
36
- const graphJson = JSON.parse(readFileSync(graphPath, 'utf-8'));
37
- return {
38
- nodeCount: graphJson.nodes?.length ?? 0,
39
- edgeCount: graphJson.edges?.length ?? 0,
40
- };
41
- }
42
- catch {
43
- return { nodeCount: 0, edgeCount: 0 };
44
- }
45
- }
46
- // ── Graph management (sync) ────────────────────────────────────────────────────
47
- /**
48
- * Build or update the knowledge graph for a workspace directory.
49
- * Writes to <cwd>/graphify-out/graph.json (graphify-ts default).
50
- * Synchronous — runs synchronously so it can be called from spawn().
51
- */
52
- export function buildGraphSync(cwd, { incremental = true } = {}) {
53
- const startTime = Date.now();
54
- const graphPath = getGraphPath(cwd);
55
- const args = ['generate', cwd];
56
- if (incremental && existsSync(graphPath)) {
57
- args.push('--update');
58
- }
59
- try {
60
- execFileSync('npx', ['graphify-ts', ...args], {
61
- cwd,
62
- stdio: 'ignore',
63
- timeout: 120_000,
64
- });
65
- }
66
- catch {
67
- // Non-zero exit is fine — graphify-ts returns 1 when no files changed.
68
- // Also swallow ENOENT (graphify-ts not installed) — degrade gracefully.
69
- }
70
- return { elapsedMs: Date.now() - startTime };
71
- }
72
- // ── Context pack compilation (sync) ────────────────────────────────────────────
73
- /**
74
- * Compile a compact context pack for a prompt + workspace.
75
- * Synchronous — calls graphify-ts pack and returns the payload.
76
- */
77
- export function compileContextPackSync(prompt, cwd, { task = 'explain', budget = 3000 } = {}) {
78
- const startTime = Date.now();
79
- const graphPath = getGraphPath(cwd);
80
- // Build graph if it doesn't exist yet
81
- if (!existsSync(graphPath)) {
82
- buildGraphSync(cwd, { incremental: false });
83
- }
84
- // Degrade gracefully if graph still doesn't exist
85
- if (!existsSync(graphPath)) {
86
- return null;
87
- }
88
- try {
89
- const output = execFileSync('npx', ['graphify-ts', 'pack', prompt, '--graph', graphPath, '--task', task, '--budget', String(budget)], {
90
- cwd,
91
- encoding: 'utf-8',
92
- maxBuffer: 512 * 1024,
93
- timeout: 30_000,
94
- });
95
- const { nodeCount, edgeCount } = getGraphStats(cwd);
96
- return {
97
- graphPath,
98
- payload: output.trim(),
99
- budget,
100
- task,
101
- nodeCount,
102
- edgeCount,
103
- elapsedMs: Date.now() - startTime,
104
- packId: randomUUID(),
105
- };
106
- }
107
- catch {
108
- // graphify-ts may return non-zero or hit timeout — degrade gracefully.
109
- return null;
110
- }
111
- }
112
- /**
113
- * Compile a context pack from a session's final state (prompt + artifacts + steps).
114
- * Used for P1 session-exit persistence and P2 handoff enrichment.
115
- */
116
- export function compileSessionPackSync(cwd, { prompt, artifacts, steps, agent }) {
117
- // Combine prompt + artifacts + steps into a rich prompt for the pack
118
- const sessionContext = [
119
- prompt,
120
- agent ? `Agent: ${agent}` : undefined,
121
- artifacts ? `Artifacts:\n${artifacts}` : undefined,
122
- steps ? `Steps taken:\n${steps}` : undefined,
123
- ]
124
- .filter(Boolean)
125
- .join('\n\n');
126
- return compileContextPackSync(sessionContext, cwd, { task: 'impact', budget: 2500 });
127
- }
128
- // ── Context pack persistence ───────────────────────────────────────────────────
129
- /**
130
- * Persist a context pack to disk for cross-session handoff (P2).
131
- * Returns the path to the packs file.
132
- */
133
- export function persistContextPack(pack, cwd) {
134
- const packsDir = join(cwd, CONTEXT_PACKS_DIR);
135
- if (!existsSync(packsDir)) {
136
- mkdirSync(packsDir, { recursive: true });
137
- }
138
- const packsFile = join(packsDir, CONTEXT_PACKS_FILE);
139
- const entry = {
140
- packId: pack.packId,
141
- timestamp: Date.now(),
142
- task: pack.task,
143
- nodeCount: pack.nodeCount,
144
- edgeCount: pack.edgeCount,
145
- graphPath: pack.graphPath,
146
- payloadPreview: pack.payload.slice(0, 500), // truncate for log
147
- payloadLength: pack.payload.length,
148
- };
149
- writeFileSync(packsFile, JSON.stringify(entry) + '\n', { flag: 'a' });
150
- return packsFile;
151
- }
152
- /**
153
- * Load the most recent context pack from disk (for P2 handoff).
154
- */
155
- export function loadRecentContextPack(cwd) {
156
- const packsFile = join(cwd, CONTEXT_PACKS_DIR, CONTEXT_PACKS_FILE);
157
- if (!existsSync(packsFile)) {
158
- return null;
159
- }
160
- try {
161
- const lines = readFileSync(packsFile, 'utf-8').split('\n').filter(Boolean);
162
- if (lines.length === 0)
163
- return null;
164
- const lastEntry = JSON.parse(lines[lines.length - 1]);
165
- return {
166
- packId: lastEntry.packId,
167
- graphPath: lastEntry.graphPath,
168
- payload: '', // payload is truncated in disk storage
169
- budget: 3000,
170
- task: lastEntry.task,
171
- nodeCount: lastEntry.nodeCount,
172
- edgeCount: lastEntry.edgeCount,
173
- elapsedMs: 0,
174
- };
175
- }
176
- catch {
177
- return null;
178
- }
179
- }
180
- // ── Prompt enrichment (sync) ───────────────────────────────────────────────────
181
- /**
182
- * Prepend a context pack to a prompt if graphify-ts is available.
183
- *
184
- * Synchronous — can be called directly from spawn().
185
- * Returns the original prompt if graphify-ts fails or is unavailable.
186
- *
187
- * Respects CODEHERE_GRAPHIFY env var:
188
- * - unset or "auto" (default): enrich ONLY from a pre-existing graph/pack;
189
- * NEVER compile on the spawn hot path (root-cause fix 2026-06-02 for
190
- * "every task calls graphify" — `npx graphify-ts pack` was shelling out
191
- * synchronously on every spawn, slowing crucial tasks + prepending a
192
- * verbose context blob the user never asked for).
193
- * - "force": compile a fresh pack on spawn (explicit opt-in for the cost).
194
- * - "off": skip entirely.
195
- */
196
- export function enrichPromptWithPackSync(prompt, cwd, { force = false } = {}) {
197
- const mode = process.env.CODEHERE_GRAPHIFY ?? 'auto';
198
- if (mode === 'off')
199
- return prompt;
200
- const forceCompile = force || mode === 'force';
201
- // 'auto' must NEVER touch the user's prompt — not by compiling (npx subprocess
202
- // on the hot path) AND not by prepending an existing pack's "[graphify context:
203
- // …]" reference blob. That blob leaked into the visible prompt, the Final
204
- // Report summary, and the conversation ("On it — [graphify context: 1017
205
- // nodes…]") — user-reported 2026-06-02 with a graph present. Structural
206
- // context enrichment is now 'force'-only opt-in; a normal task gets a clean prompt.
207
- if (!forceCompile)
208
- return prompt;
209
- // Compile a fresh pack (force only)
210
- // Compile a fresh pack (force only)
211
- const pack = compileContextPackSync(prompt, cwd, { task: 'explain', budget: 2500 });
212
- if (!pack) {
213
- return prompt;
214
- }
215
- // Persist for P2 handoff
216
- persistContextPack(pack, cwd);
217
- // Prepend context pack as a system hint so the agent sees it first
218
- return [
219
- `[graphify-ts context pack — ${pack.nodeCount} nodes, ${pack.edgeCount} edges, ${pack.elapsedMs}ms]`,
220
- pack.payload,
221
- '---',
222
- prompt,
223
- ].join('\n\n');
224
- }
225
- // ── Graph querying (sync) ──────────────────────────────────────────────────────
226
- /**
227
- * Query the knowledge graph for a question.
228
- * Useful for PM-brain routing and structured context handoff (P2).
229
- */
230
- export function queryGraphSync(question, cwd, { maxTokens = 2000 } = {}) {
231
- const startTime = Date.now();
232
- const graphPath = getGraphPath(cwd);
233
- if (!existsSync(graphPath)) {
234
- return null;
235
- }
236
- try {
237
- const output = execFileSync('npx', ['graphify-ts', 'query', question, '--graph', graphPath, '--budget', String(maxTokens)], {
238
- cwd,
239
- encoding: 'utf-8',
240
- maxBuffer: 256 * 1024,
241
- timeout: 15_000,
242
- });
243
- return {
244
- question,
245
- answer: output.trim(),
246
- elapsedMs: Date.now() - startTime,
247
- graphPath,
248
- };
249
- }
250
- catch {
251
- return null;
252
- }
253
- }
254
- //# sourceMappingURL=graphify-context.js.map
@@ -1,380 +0,0 @@
1
- /**
2
- * F3 Task 8 — memory injection into spawned-agent prompts.
3
- *
4
- * Builds a prompt prefix from the project's `.codehere/` memory layers
5
- * (AGENTS.md briefing + working/session.md + semantic/lessons.jsonl + a
6
- * slice of episodic/repo_map.json topSymbols) so spawned agents start
7
- * warm — with the project's conventions, in-flight context, accumulated
8
- * lessons, and the most-referenced symbols already in their context window.
9
- *
10
- * Pure, side-effect-free, fully unit-tested. Each adapter calls
11
- * `buildMemoryPrefix(projectRoot)` near the top of `spawn()`, prepends
12
- * the resulting prefix to the user's prompt, and exposes the metadata via
13
- * `TaskSession.memoryInjection`.
14
- *
15
- * Token budget is char-count-approximated (4 chars ≈ 1 token, the
16
- * Anthropic / OpenAI rule-of-thumb). Default cap is 3000 tokens —
17
- * generous but well under any realistic context window. When budget is
18
- * exceeded, sections are dropped in priority order:
19
- *
20
- * keep AGENTS.md — never dropped if user-edited (the briefing
21
- * is the primary value)
22
- * drop 3 repo_map slice — symbols are nice-to-have; the agent can
23
- * discover them via grep if missing
24
- * drop 2 lessons — useful but not load-bearing
25
- * drop 1 session.md — useful for hand-off but redundant if the
26
- * user re-states their task
27
- *
28
- * Default-seed detection skips placeholder content (the init seeds for
29
- * AGENTS.md / session.md). Same byte-equality-based detection pattern as
30
- * memory-command.ts (per feedback_implementation_patterns_v05.md Pattern #1).
31
- */
32
- import { existsSync, readFileSync, realpathSync, writeFileSync } from 'fs';
33
- import { join, dirname, resolve } from 'path';
34
- const DEFAULT_MAX_TOKENS = 3000;
35
- const DEFAULT_MAX_LESSONS = 10;
36
- const DEFAULT_MAX_SYMBOLS = 30;
37
- // 3 chars per token — conservative ratio per K2 Round 1 feedback. Code
38
- // tokenizes denser than prose under typical BPE, so the safer-defaults 4/1
39
- // rule-of-thumb underestimates by 1.5-2x for code-heavy prefixes (which
40
- // repo_map + lessons.jsonl frequently are). 3/1 stays well clear of model
41
- // per-message caps without dropping useful content from typical repos.
42
- const CHARS_PER_TOKEN = 3;
43
- /** Char-count → token estimate. Conservative — actual tokenization may differ. */
44
- function estimateTokens(s) {
45
- return Math.ceil(s.length / CHARS_PER_TOKEN);
46
- }
47
- /**
48
- * Detect whether the given file content is still the init-time placeholder.
49
- * If so, the user hasn't customized it — injecting the placeholder would
50
- * just confuse the agent with template instructions.
51
- *
52
- * Heuristic: presence of bracketed-instruction lines like `[One-paragraph
53
- * description...]` or the `> Per-machine, gitignored` boilerplate, AND no
54
- * substantive non-template content after the markers. Conservative — when
55
- * in doubt, INCLUDE rather than exclude (better to over-share than miss
56
- * the briefing).
57
- */
58
- function looksLikeInitSeed(content, kind) {
59
- if (kind === 'agents') {
60
- // The init AGENTS/CONTEXT seed has these distinctive bracketed placeholders.
61
- // If at least 2 of them are present, treat as untouched.
62
- const markers = [
63
- /\[One-paragraph description of what this repo does/,
64
- /\[How this codebase is organized/,
65
- /\[Tone for generated code\./,
66
- /\[Don't do X\. Always do Y\./,
67
- ];
68
- const hits = markers.filter((re) => re.test(content)).length;
69
- return hits >= 2;
70
- }
71
- if (kind === 'session') {
72
- // The init session.md seed has this header + structure
73
- const markers = [/^# Working session/m, /^> Per-machine, gitignored/m, /^## In flight\s*\n+- \[ \]\s*$/m];
74
- const hits = markers.filter((re) => re.test(content)).length;
75
- return hits >= 2;
76
- }
77
- return false;
78
- }
79
- /**
80
- * Read the project briefing. Search order (highest-priority first):
81
- * 1. <projectRoot>/AGENTS.md — agentic-stack convention; if the user
82
- * has this file at the repo root with real content, it IS their
83
- * canonical briefing and codehere must respect it (Bug 2 from the
84
- * Apr 28 EXP-2 dogfood on `~/code/clipa`).
85
- * 2. <codehereDir>/AGENTS.md — F3-canonical (codehere's own)
86
- * 3. <codehereDir>/CONTEXT.md — v0.4.1 back-compat
87
- *
88
- * Returns the body string + the path that was read (for the prefix
89
- * header), or null if no usable briefing exists at any tier (every
90
- * candidate is missing, empty, or looks like the init placeholder).
91
- */
92
- function readBriefing(projectRoot, codehereDir) {
93
- const candidates = [
94
- { path: join(projectRoot, 'AGENTS.md'), label: 'AGENTS.md (repo root)' },
95
- { path: join(codehereDir, 'AGENTS.md'), label: '.codehere/AGENTS.md' },
96
- { path: join(codehereDir, 'CONTEXT.md'), label: '.codehere/CONTEXT.md' },
97
- ];
98
- for (const c of candidates) {
99
- if (existsSync(c.path)) {
100
- try {
101
- const body = readFileSync(c.path, 'utf8').trim();
102
- if (body.length === 0)
103
- continue;
104
- if (looksLikeInitSeed(body, 'agents'))
105
- continue; // placeholder — try next
106
- return { body, sourcePath: c.label };
107
- }
108
- catch {
109
- continue;
110
- }
111
- }
112
- }
113
- return null;
114
- }
115
- /** Read working/session.md. Skip if seed-shaped or empty. */
116
- function readSession(codehereDir) {
117
- const p = join(codehereDir, 'memory/working/session.md');
118
- if (!existsSync(p))
119
- return null;
120
- try {
121
- const body = readFileSync(p, 'utf8').trim();
122
- if (body.length === 0)
123
- return null;
124
- if (looksLikeInitSeed(body, 'session'))
125
- return null;
126
- return body;
127
- }
128
- catch {
129
- return null;
130
- }
131
- }
132
- /** Read up to N lessons from semantic/lessons.jsonl. */
133
- function readLessons(codehereDir, n) {
134
- const p = join(codehereDir, 'memory/semantic/lessons.jsonl');
135
- if (!existsSync(p))
136
- return [];
137
- try {
138
- const body = readFileSync(p, 'utf8');
139
- return body
140
- .split('\n')
141
- .filter((line) => line.trim().length > 0)
142
- .slice(0, n)
143
- .map((line) => {
144
- try {
145
- const obj = JSON.parse(line);
146
- return typeof obj.text === 'string' ? obj.text : line;
147
- }
148
- catch {
149
- return line;
150
- }
151
- });
152
- }
153
- catch {
154
- return [];
155
- }
156
- }
157
- function readTopSymbols(codehereDir, n) {
158
- const p = join(codehereDir, 'memory/episodic/repo_map.json');
159
- if (!existsSync(p))
160
- return [];
161
- try {
162
- const body = JSON.parse(readFileSync(p, 'utf8'));
163
- if (!Array.isArray(body.topSymbols))
164
- return [];
165
- return body.topSymbols.slice(0, n);
166
- }
167
- catch {
168
- return [];
169
- }
170
- }
171
- /**
172
- * Format individual sections. Each returns `null` if empty/skipped.
173
- */
174
- function formatBriefing(briefing, sourcePath) {
175
- return `# Project briefing (${sourcePath})\n\n${briefing}\n`;
176
- }
177
- function formatSession(session) {
178
- return `# Current session scratch (.codehere/memory/working/session.md)\n\n${session}\n`;
179
- }
180
- function formatLessons(lessons) {
181
- const items = lessons.map((l) => `- ${l}`).join('\n');
182
- return `# Project lessons (.codehere/memory/semantic/lessons.jsonl)\n\n${items}\n`;
183
- }
184
- function formatSymbols(symbols) {
185
- const lines = symbols.map((s) => `- \`${s.name}\` — ${s.file}:${s.line} (${s.references} refs)`).join('\n');
186
- return `# Top symbols in this repo (.codehere/memory/episodic/repo_map.json)\n\n${lines}\n`;
187
- }
188
- /**
189
- * Build the memory-injection prefix for the project at `projectRoot`.
190
- *
191
- * If `.codehere/` doesn't exist, returns empty (nothing to inject).
192
- * Otherwise reads the four sections, formats each, fits to the token
193
- * budget by dropping low-priority sections first.
194
- */
195
- export function buildMemoryPrefix(projectRoot, options = {}) {
196
- const codehereDir = join(projectRoot, '.codehere');
197
- const hasCodehereDir = existsSync(codehereDir);
198
- const maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;
199
- const maxLessons = options.maxLessons ?? DEFAULT_MAX_LESSONS;
200
- const maxSymbols = options.maxSymbols ?? DEFAULT_MAX_SYMBOLS;
201
- // Briefing discovery is INDEPENDENT of .codehere/ presence — a pure
202
- // agentic-stack repo (root AGENTS.md, no .codehere/) is a valid
203
- // injection source on its own (codex Round 1 fix on Bug 2). Only
204
- // session/lessons/repo_map require the .codehere/ tree.
205
- const briefing = readBriefing(projectRoot, codehereDir);
206
- const session = hasCodehereDir ? readSession(codehereDir) : null;
207
- const lessons = hasCodehereDir ? readLessons(codehereDir, maxLessons) : [];
208
- const symbols = hasCodehereDir ? readTopSymbols(codehereDir, maxSymbols) : [];
209
- // Build candidate sections in PRIORITY order (most-essential first).
210
- // Drop direction is REVERSE — least-essential drops first.
211
- const candidates = [];
212
- if (briefing)
213
- candidates.push({ key: 'agents', body: formatBriefing(briefing.body, briefing.sourcePath) });
214
- if (session)
215
- candidates.push({ key: 'session', body: formatSession(session) });
216
- if (lessons.length > 0)
217
- candidates.push({ key: 'lessons', body: formatLessons(lessons) });
218
- if (symbols.length > 0)
219
- candidates.push({ key: 'repo_map', body: formatSymbols(symbols) });
220
- if (candidates.length === 0) {
221
- return { prefix: '', included: [], tokens: 0, truncated: false };
222
- }
223
- // Closing separator, always added at the end.
224
- const SEPARATOR = '\n---\n\n';
225
- const separatorTokens = estimateTokens(SEPARATOR);
226
- // Drop from least-essential (last) until everything fits in budget.
227
- let truncated = false;
228
- let kept = [...candidates];
229
- while (kept.length > 0) {
230
- const totalChars = kept.reduce((acc, c) => acc + c.body.length, 0);
231
- const totalTokens = estimateTokens(kept.map((c) => c.body).join('\n')) + separatorTokens;
232
- if (totalTokens <= maxTokens)
233
- break;
234
- // Drop the last (least-essential) section.
235
- kept.pop();
236
- truncated = true;
237
- void totalChars;
238
- }
239
- // Edge: even AGENTS alone is over budget — truncate AGENTS body itself.
240
- if (kept.length === 1 && estimateTokens(kept[0].body) + separatorTokens > maxTokens) {
241
- const room = (maxTokens - separatorTokens) * CHARS_PER_TOKEN;
242
- const truncatedBody = kept[0].body.slice(0, Math.max(0, room - 32)) + '\n... [truncated to fit]\n';
243
- kept = [{ key: kept[0].key, body: truncatedBody }];
244
- truncated = true;
245
- }
246
- if (kept.length === 0) {
247
- return { prefix: '', included: [], tokens: 0, truncated };
248
- }
249
- const prefix = kept.map((c) => c.body).join('\n') + SEPARATOR;
250
- return {
251
- prefix,
252
- included: kept.map((c) => c.key),
253
- tokens: estimateTokens(prefix),
254
- truncated,
255
- };
256
- }
257
- /**
258
- * Walk up from `startPath` looking for the nearest ancestor (inclusive)
259
- * that codehere can inject memory FROM — meaning EITHER:
260
- * - has a `.codehere/` directory (codehere-initialized project), OR
261
- * - has a root-level `AGENTS.md` (agentic-stack convention)
262
- *
263
- * Returns the REAL filesystem ancestor path (symlinks resolved), or
264
- * `null` if no such ancestor exists.
265
- *
266
- * codex Round 1 BLOCKER fix: spawning a task from a subdirectory (e.g.
267
- * `cd src/auth && codehere task ...`) would miss the project root if we
268
- * only used the immediate cwd. Hence the upward walk.
269
- *
270
- * codex Round 2 BLOCKER fix: the upward walk must walk the REAL
271
- * filesystem ancestry, not the LEXICAL one. Without `realpathSync`,
272
- * `cwd` inside a symlinked subtree ascends through the lexical path and
273
- * misses the real project root. Real scenario for iCloud-symlinked /
274
- * shared-volume / mounted-drive projects.
275
- *
276
- * Bug 2 Round 2 BLOCKER fix: also accept a project root that has a
277
- * root-level `AGENTS.md` even WITHOUT `.codehere/`. A pure agentic-stack
278
- * repo (clipa-style) is a valid injection source on its own.
279
- *
280
- * Stops at the filesystem root. Symlink-following on read is the right
281
- * asymmetry per ADR-012 (the symlink defense applies to writes, not reads).
282
- *
283
- * Exported for the daemon's /api/memory/status route (the room's init nudge
284
- * must report the SAME readiness the injector itself would act on).
285
- */
286
- export function findProjectRoot(startPath) {
287
- let current;
288
- try {
289
- current = realpathSync(resolve(startPath));
290
- }
291
- catch {
292
- // Broken symlink at startPath itself — fall back to lexical resolve.
293
- // (Better to attempt some lookup than fail outright.)
294
- current = resolve(startPath);
295
- }
296
- // Bound the loop at the filesystem root by checking dirname stability.
297
- for (;;) {
298
- if (existsSync(join(current, '.codehere')) || existsSync(join(current, 'AGENTS.md'))) {
299
- return current;
300
- }
301
- const parent = dirname(current);
302
- if (parent === current)
303
- return null;
304
- current = parent;
305
- }
306
- }
307
- /**
308
- * Adapter-side convenience: turn a raw user prompt into the spawn-ready
309
- * prompt + the diagnostic record. Each of the 5 built-in adapters calls
310
- * this near the top of `spawn()` and uses the returned `prompt` going
311
- * forward, plus exposes `record` via `TaskSession.memoryInjection`.
312
- *
313
- * Walks UPWARD from `cwd` to find the nearest ancestor with `.codehere/`,
314
- * so spawning from a subdirectory still gets injection (codex Round 1 fix).
315
- *
316
- * Returns `{ prompt: <unchanged>, record: undefined }` when:
317
- * - `options.noMemoryPrefix` is true (caller opted out)
318
- * - no ancestor of `cwd` has `.codehere/` (nothing to inject)
319
- * - all sections are empty / placeholder seeds
320
- */
321
- export function applyMemoryPrefix(prompt, cwd, options) {
322
- if (options.noMemoryPrefix)
323
- return { prompt, record: undefined };
324
- const projectRoot = findProjectRoot(cwd);
325
- if (projectRoot === null)
326
- return { prompt, record: undefined };
327
- const r = buildMemoryPrefix(projectRoot);
328
- if (r.prefix === '')
329
- return { prompt, record: undefined };
330
- return {
331
- prompt: r.prefix + prompt,
332
- record: { included: r.included, tokens: r.tokens, truncated: r.truncated },
333
- };
334
- }
335
- /**
336
- * Write-back side of the shared-room hand-off: append a finished turn's outcome
337
- * to the project's `working/session.md` so the NEXT agent spawned in this
338
- * project reads it via buildMemoryPrefix() — closing the loop ("agents take
339
- * turns in a shared room, each building on the last").
340
- *
341
- * Resolves the SAME project root as the read path (findProjectRoot), so it only
342
- * writes when the project is codehere-initialized — it never creates `.codehere/`
343
- * in an uninitialized repo. The scratch is capped (the injector also budgets it).
344
- * Best-effort: a memory-write failure must never break a run.
345
- */
346
- export function recordTurnToWorkingMemory(cwd, entry) {
347
- try {
348
- const projectRoot = findProjectRoot(cwd);
349
- if (projectRoot === null)
350
- return; // not a codehere-initialized project
351
- const dir = join(projectRoot, '.codehere', 'memory', 'working');
352
- if (!existsSync(dir))
353
- return;
354
- const p = join(dir, 'session.md');
355
- const HEADER = '# Working session log - codehere auto-records each agent turn so the next one picks up here\n';
356
- const agent = (entry.agent || 'agent').replace(/\s+/g, ' ').trim().slice(0, 40);
357
- const prompt = (entry.prompt || '').replace(/\s+/g, ' ').trim().slice(0, 120);
358
- const summary = (entry.summary || '').replace(/\s+/g, ' ').trim().slice(0, 600);
359
- const files = entry.files ? ` - ${entry.files} file${entry.files === 1 ? '' : 's'} changed` : '';
360
- const block = `\n## ${agent}${prompt ? ' - ' + prompt : ''}${files}\n${summary || '(no written summary)'}\n`;
361
- let cur = '';
362
- try {
363
- cur = existsSync(p) ? readFileSync(p, 'utf8') : '';
364
- }
365
- catch {
366
- cur = '';
367
- }
368
- if (!cur.trim() || looksLikeInitSeed(cur, 'session'))
369
- cur = HEADER;
370
- cur += block;
371
- const CAP = 8000;
372
- if (cur.length > CAP)
373
- cur = HEADER + cur.slice(cur.length - (CAP - HEADER.length));
374
- writeFileSync(p, cur, 'utf8');
375
- }
376
- catch {
377
- /* never break a run on a memory-write failure */
378
- }
379
- }
380
- //# sourceMappingURL=memory-injector.js.map