codehere 0.6.0 → 0.6.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.
- package/README.md +8 -8
- package/dist/application/use-cases/generate-final-report.js +57 -2
- package/dist/application/use-cases/generate-narration.js +9 -3
- package/dist/index.js +34 -4
- package/dist/infrastructure/cost/budget-config.js +1 -1
- package/dist/infrastructure/cost/spend-aggregator.js +5 -5
- package/dist/infrastructure/hooks/claude-sdk-hook.js +48 -2
- package/dist/infrastructure/orchestration/codex-adapter.js +11 -6
- package/dist/infrastructure/orchestration/context-handoff.js +1 -1
- package/dist/infrastructure/orchestration/gemini-adapter.js +3 -15
- package/dist/infrastructure/orchestration/graphify-context.js +6 -13
- package/dist/infrastructure/orchestration/memory-injector.js +11 -16
- package/dist/infrastructure/orchestration/opencode-adapter.js +42 -1
- package/dist/infrastructure/orchestration/progress-aggregator.js +1 -2
- package/dist/infrastructure/orchestration/session-manager.js +178 -76
- package/dist/infrastructure/protocols/cloud-sync.js +9 -5
- package/dist/infrastructure/protocols/mcp-server-impl.js +368 -334
- package/dist/infrastructure/storage/cross-tool-agents.js +19 -2
- package/dist/infrastructure/storage/project-thread-migration.js +12 -7
- package/dist/infrastructure/storage/project-thread-reader.js +11 -1
- package/dist/infrastructure/storage/project-thread-title-backfill.js +6 -2
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/init-command.js +5 -4
- package/dist/presentation/cli/commands/memory-command.js +29 -15
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/repo-map-builder.js +4 -4
- package/dist/presentation/cli/repl.js +20 -1
- package/dist/ui.js +36 -28
- package/dist/utils/typo-guard.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
[](https://www.npmjs.com/package/codehere)
|
|
7
7
|
[](../LICENSE)
|
|
8
|
-
[]()
|
|
9
9
|
[]()
|
|
10
10
|
|
|
11
11
|
**Codehere is the vendor-neutral layer between you and the coding agents you already use** — Claude Code, Codex, Aider, Gemini, OpenCode. One `npm install`. Local-first, OSS, no account, no IAM, no compliance prereqs. Switch agent, keep your memory. End the day with one Final Report covering everything they did.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
All 5 adapters (Claude Code, Codex, Aider, Gemini, OpenCode) get session-tracing, Final Reports, and trust scoring. Claude Code additionally gets full pre-execution safety-hook enforcement on every tool call. The *memory*, *audit log*, *failover*, and *Final Report* surfaces ship across all 5 adapters today.
|
|
14
14
|
|
|
15
15
|
The enterprise tier is owned by [OpenAI Frontier](https://openai.com/index/introducing-openai-frontier/) (8-14 weeks of deploy prereqs, sales-team gated) and [Anthropic Cowork](https://www.anthropic.com/product/claude-cowork) (paid Claude subscription, single-vendor). Codehere fills the gap they structurally can't reach: **the individual / small-team / no-IT-department tier, vendor-neutral**.
|
|
16
16
|
|
|
@@ -29,7 +29,7 @@ One layer, every agent, your work stays portable.
|
|
|
29
29
|
|
|
30
30
|
| | | |
|
|
31
31
|
|---|---|---|
|
|
32
|
-
| 1 | **Install** | `npm install -g codehere
|
|
32
|
+
| 1 | **Install** | `npm install -g codehere` (Node 20+) — v0.6.0 is the current stable |
|
|
33
33
|
| 2 | **Detect environment** | `codehere setup` — checks API key, finds your repo |
|
|
34
34
|
| 3 | **Install the safety hook** | `codehere hook install` — every Claude Code tool call now audited |
|
|
35
35
|
| 4 | **Install at least one specialist** | Claude Code: `npm i -g @anthropic-ai/claude-code` (Codex / Aider / OpenCode / Gemini also supported · see [docs/integrations/](../docs/integrations/) for Cursor MCP setup) |
|
|
@@ -51,7 +51,7 @@ Context + End State + Constraints
|
|
|
51
51
|
|
|
52
52
|
### Folder convention (what `codehere init` writes)
|
|
53
53
|
|
|
54
|
-
The v0.
|
|
54
|
+
The v0.6 layout — F3 project memory:
|
|
55
55
|
|
|
56
56
|
| Path | Purpose | Committed? |
|
|
57
57
|
|---|---|---|
|
|
@@ -90,9 +90,9 @@ codehere report # generates a shareable markdown audit report
|
|
|
90
90
|
codehere status # quick dashboard · sessions, blocks, trust trends
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
The hook intercepts every `Bash`, `Edit`, `Write`, `MultiEdit`, and `NotebookEdit` call from any Claude Code session running on your machine, runs it through the trust-scorer pattern bank in under 5 ms, blocks anything matching the destructive / credential / injection patterns, and appends every decision to an append-only JSONL audit log at `~/.codehere/audit/`.
|
|
93
|
+
The hook intercepts every `Bash`, `Edit`, `Write`, `MultiEdit`, and `NotebookEdit` call from any Claude Code session running on your machine, runs it through the trust-scorer pattern bank in under 5 ms, blocks anything matching the destructive / credential / injection patterns, and appends every decision to an append-only JSONL audit log at `~/.codehere/audit/`. Codex, Aider, OpenCode, and Gemini adapters spawn through their native CLIs and emit to `~/.codehere/sessions.jsonl` with per-tool tracing via the cross-vendor mirror hook.
|
|
94
94
|
|
|
95
|
-
## Memory · F3 (v0.
|
|
95
|
+
## Memory · F3 (v0.6)
|
|
96
96
|
|
|
97
97
|
Once you've run `codehere init` in a project, every spawned agent — Claude Code, Codex, Aider, OpenCode, or Gemini — automatically reads the project's memory on entry. The agent starts warm with the briefing, recent lessons, and the most-referenced symbols in your codebase, instead of cold-grepping every session.
|
|
98
98
|
|
|
@@ -124,7 +124,7 @@ codehere memory show protocols # safety patterns by category
|
|
|
124
124
|
|
|
125
125
|
**Other memory commands:**
|
|
126
126
|
- `codehere memory init` — migrate the v0.4.1 layout (`CONTEXT.md` / `HANDOVER.md` / `PROMPTS.md`) into the F3 layout (one shot, leaves back-compat symlinks; `--dry-run` previews)
|
|
127
|
-
- `codehere memory repo-map [--rebuild]` — build the symbol map;
|
|
127
|
+
- `codehere memory repo-map [--rebuild]` — build the symbol map; currently TS-native MVP
|
|
128
128
|
- `codehere protocols export [--rebuild]` — write the trust-scorer pattern bank to `.codehere/protocols/hook_patterns.json` for inspection / cross-tool ingestion
|
|
129
129
|
|
|
130
130
|
## Going deeper
|
|
@@ -204,7 +204,7 @@ For depth beyond this README:
|
|
|
204
204
|
## Requirements
|
|
205
205
|
|
|
206
206
|
- **Node.js 20+** (pinned in `.nvmrc` and `engines.node`)
|
|
207
|
-
- **macOS or Linux** · Windows via WSL works for development
|
|
207
|
+
- **macOS or Linux** · Windows via WSL works for development
|
|
208
208
|
- **Optional but recommended:** at least one supported agent (Claude Code, Codex, Aider, OpenCode, or Gemini) installed before running `codehere start`
|
|
209
209
|
|
|
210
210
|
## License
|
|
@@ -61,7 +61,9 @@ ${i.steps.length ? i.steps.map((s) => `- ${s.label} (${s.state})`).join('\n') :
|
|
|
61
61
|
|
|
62
62
|
Files changed:
|
|
63
63
|
${i.artifacts?.files?.length
|
|
64
|
-
? i.artifacts.files
|
|
64
|
+
? i.artifacts.files
|
|
65
|
+
.map((f) => `- ${f.path} (+${f.added}/-${f.removed}${f.trust !== undefined ? `, trust: ${f.trust}` : ''})`)
|
|
66
|
+
.join('\n')
|
|
65
67
|
: '(no file artifacts captured)'}
|
|
66
68
|
|
|
67
69
|
Token usage: ${i.artifacts?.usage?.inputTokens ?? 0} in / ${i.artifacts?.usage?.outputTokens ?? 0} out
|
|
@@ -75,16 +77,69 @@ function selectProviderBinary() {
|
|
|
75
77
|
return [override];
|
|
76
78
|
return ['claude-free', 'claude'];
|
|
77
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* UAT-003 — patterns that indicate the wrapped agent's session terminated
|
|
82
|
+
* because of a network / auth / quota failure rather than a real task.
|
|
83
|
+
* When we see these in the would-be summary text, we prepend a plain-English
|
|
84
|
+
* recovery hint instead of letting the raw provider error echo through.
|
|
85
|
+
*
|
|
86
|
+
* Pre-fix: bare `exit` in REPL → Conductor spawned claude-code → real API
|
|
87
|
+
* call failed with "All connection attempts failed Request ID: req_...";
|
|
88
|
+
* Final Report quoted that text verbatim. Per Core Values #4 + #6, this is
|
|
89
|
+
* unacceptable — the user sees a Request ID that means nothing.
|
|
90
|
+
*/
|
|
91
|
+
const NETWORK_FAILURE_PATTERNS = /(?:all connection attempts failed|ECONNREFUSED|ENOTFOUND|fetch failed|network error|request id:\s*req_|connection timed out|getaddrinfo)/i;
|
|
92
|
+
function isLikelyNetworkFailureSummary(text) {
|
|
93
|
+
return NETWORK_FAILURE_PATTERNS.test(text);
|
|
94
|
+
}
|
|
95
|
+
function buildErrorRecoveryReport(input, durationMs) {
|
|
96
|
+
// Tailor the hint to the specific failure shape we can detect from input.
|
|
97
|
+
const agentName = input.agent === 'conductor' ? 'the agent codehere chose' : input.agent;
|
|
98
|
+
return {
|
|
99
|
+
summary: `The ${agentName} session ended without completing the task. ` +
|
|
100
|
+
`This usually means a network hiccup, an expired login, or a rate limit on the provider.\n\n` +
|
|
101
|
+
`What to try: check your internet, run \`codehere health\` to confirm reachability + auth, ` +
|
|
102
|
+
`or wait a minute and rerun the same prompt. If it keeps happening, try a different agent ` +
|
|
103
|
+
`from the dropdown — codehere is vendor-neutral on purpose.\n\n` +
|
|
104
|
+
`No files were changed in this attempt.`,
|
|
105
|
+
generatedAt: Date.now(),
|
|
106
|
+
durationMs,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
78
109
|
/**
|
|
79
110
|
* Generate a Final Report. Returns null if no provider is reachable.
|
|
80
111
|
* Never throws — failure modes return null + log a debug line.
|
|
112
|
+
*
|
|
113
|
+
* UAT-003 — when the input.status is 'error', we skip the LLM call entirely
|
|
114
|
+
* and return a deterministic recovery hint. Saves tokens on a known-failed
|
|
115
|
+
* task AND gives the user a plain-English next step instead of waiting for
|
|
116
|
+
* a possibly-unhelpful LLM summary.
|
|
81
117
|
*/
|
|
82
118
|
export async function generateFinalReport(input) {
|
|
119
|
+
// Short-circuit on errored sessions — don't waste tokens summarizing nothing.
|
|
120
|
+
if (input.status === 'error') {
|
|
121
|
+
return buildErrorRecoveryReport(input, 0);
|
|
122
|
+
}
|
|
83
123
|
const prompt = PROMPT_TEMPLATE(input);
|
|
84
124
|
for (const binary of selectProviderBinary()) {
|
|
85
125
|
const result = await tryProvider(binary, prompt, input.sessionId);
|
|
86
|
-
if (result)
|
|
126
|
+
if (result) {
|
|
127
|
+
// Defense-in-depth: even on 'completed' status, if the generated
|
|
128
|
+
// summary leaks a provider network-failure string (e.g., the wrapped
|
|
129
|
+
// agent exited 0 but its assistant text was an error message), wrap
|
|
130
|
+
// the summary with a recovery preamble.
|
|
131
|
+
if (isLikelyNetworkFailureSummary(result.summary)) {
|
|
132
|
+
return {
|
|
133
|
+
summary: `Couldn't reach the provider cleanly — the agent's session text looks like a network or auth failure.\n\n` +
|
|
134
|
+
`What to try: check your internet, run \`codehere health\`, or rerun in a minute. ` +
|
|
135
|
+
`If it persists, switch agents from the dropdown.\n\n` +
|
|
136
|
+
`— Raw agent message —\n${result.summary}`,
|
|
137
|
+
generatedAt: result.generatedAt,
|
|
138
|
+
durationMs: result.durationMs,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
87
141
|
return result;
|
|
142
|
+
}
|
|
88
143
|
}
|
|
89
144
|
return null;
|
|
90
145
|
}
|
|
@@ -75,11 +75,17 @@ function tryProvider(binary, prompt, sessionId, stepCount) {
|
|
|
75
75
|
try {
|
|
76
76
|
child.kill();
|
|
77
77
|
}
|
|
78
|
-
catch {
|
|
78
|
+
catch {
|
|
79
|
+
/* ignore */
|
|
80
|
+
}
|
|
79
81
|
finish(null);
|
|
80
82
|
}, 20_000);
|
|
81
|
-
child.stdout?.on('data', (chunk) => {
|
|
82
|
-
|
|
83
|
+
child.stdout?.on('data', (chunk) => {
|
|
84
|
+
stdout += chunk.toString('utf-8');
|
|
85
|
+
});
|
|
86
|
+
child.stderr?.on('data', (chunk) => {
|
|
87
|
+
stderr += chunk.toString('utf-8');
|
|
88
|
+
});
|
|
83
89
|
child.on('close', (code) => {
|
|
84
90
|
clearTimeout(timeout);
|
|
85
91
|
if (code !== 0) {
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,16 @@ import { config } from 'dotenv';
|
|
|
15
15
|
import { join, dirname } from 'path';
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
17
|
import { homedir } from 'os';
|
|
18
|
+
// ── Global crash safety ───────────────────────────────────────────────────
|
|
19
|
+
// Unhandled rejections and exceptions must never silently kill the daemon.
|
|
20
|
+
process.on('unhandledRejection', (reason) => {
|
|
21
|
+
console.error('[codehere] unhandledRejection:', reason instanceof Error ? reason.stack : reason);
|
|
22
|
+
});
|
|
23
|
+
process.on('uncaughtException', (err) => {
|
|
24
|
+
console.error('[codehere] uncaughtException:', err.stack);
|
|
25
|
+
// persist what we can before dying
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
});
|
|
18
28
|
import { executeSetupCommand } from './presentation/cli/commands/setup-command.js';
|
|
19
29
|
import { executeInitCommand } from './presentation/cli/commands/init-command.js';
|
|
20
30
|
import { executeMemoryCommand } from './presentation/cli/commands/memory-command.js';
|
|
@@ -67,7 +77,7 @@ program
|
|
|
67
77
|
// typo guard miss the unknown-command shape. See typo-guard.ts.
|
|
68
78
|
.argument('[query...]', 'Optional initial task to delegate (otherwise enter interactive REPL)')
|
|
69
79
|
.action(async (queryParts) => {
|
|
70
|
-
const joined =
|
|
80
|
+
const joined = queryParts && queryParts.length > 0 ? queryParts.join(' ') : '';
|
|
71
81
|
// One-shot delegate: `codehere "review src/auth.ts"` runs that task and exits.
|
|
72
82
|
// Useful for scripting or quick one-offs without entering the REPL.
|
|
73
83
|
if (joined && joined.trim()) {
|
|
@@ -464,6 +474,7 @@ program
|
|
|
464
474
|
console.log(colors.dim('\ncodehere MCP server stopped'));
|
|
465
475
|
process.exit(0);
|
|
466
476
|
});
|
|
477
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
467
478
|
await new Promise((resolve) => {
|
|
468
479
|
server.on('close', resolve);
|
|
469
480
|
});
|
|
@@ -565,15 +576,34 @@ program
|
|
|
565
576
|
startCloudSync();
|
|
566
577
|
}
|
|
567
578
|
await openBrowser();
|
|
568
|
-
|
|
579
|
+
const shutdown = () => {
|
|
580
|
+
try {
|
|
581
|
+
const { SessionManager } = require('./infrastructure/orchestration/session-manager.js');
|
|
582
|
+
SessionManager.getInstance().shutdown();
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
/* graceful degradation — daemon may not have started fully */
|
|
586
|
+
}
|
|
569
587
|
if (cloud) {
|
|
570
|
-
|
|
571
|
-
|
|
588
|
+
try {
|
|
589
|
+
const { stopCloudSync } = require('./infrastructure/protocols/cloud-sync.js');
|
|
590
|
+
stopCloudSync();
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
/* cloud sync may not be loaded */
|
|
594
|
+
}
|
|
572
595
|
}
|
|
573
596
|
server.close();
|
|
597
|
+
};
|
|
598
|
+
process.on('SIGINT', () => {
|
|
599
|
+
shutdown();
|
|
574
600
|
console.log(colors.dim('\ncodehere stopped'));
|
|
575
601
|
process.exit(0);
|
|
576
602
|
});
|
|
603
|
+
process.on('SIGTERM', () => {
|
|
604
|
+
shutdown();
|
|
605
|
+
process.exit(0);
|
|
606
|
+
});
|
|
577
607
|
await new Promise((resolve) => {
|
|
578
608
|
server.on('close', resolve);
|
|
579
609
|
});
|
|
@@ -84,7 +84,7 @@ export function saveBudget(cfg) {
|
|
|
84
84
|
mkdirSync(dir, { recursive: true });
|
|
85
85
|
const lines = [
|
|
86
86
|
'# codehere daily-spend budget. Edit by hand or via `codehere budget set <amount>`.',
|
|
87
|
-
|
|
87
|
+
"# daily_usd: hard ceiling for today's estimated spend. Set to a number to enable.",
|
|
88
88
|
'# Leave commented out / omit to disable budget enforcement entirely.',
|
|
89
89
|
];
|
|
90
90
|
if (cfg.dailyUsd !== null) {
|
|
@@ -36,11 +36,11 @@ function sessionsFile() {
|
|
|
36
36
|
* under-promise and miss a real bill. */
|
|
37
37
|
const COST_PER_M_OUTPUT_TOKENS_USD = {
|
|
38
38
|
'claude-code': 15.0, // Sonnet 4.6 baseline
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
codex: 10.0, // GPT-5 / codex baseline
|
|
40
|
+
aider: 15.0, // typically Sonnet
|
|
41
|
+
gemini: 0, // free tier (Google AI Studio)
|
|
42
|
+
opencode: 0, // typically free local / NIM-routed
|
|
43
|
+
conductor: 15.0, // routes to Claude Code by default
|
|
44
44
|
};
|
|
45
45
|
const INPUT_RATE_RATIO = 0.2; // input ≈ 1/5 of output
|
|
46
46
|
const CACHE_READ_RATIO = 0.1; // cache reads ≈ 1/10 of input (so 1/50 output)
|
|
@@ -13,13 +13,20 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Docs: https://docs.anthropic.com/en/docs/claude-code/hooks
|
|
15
15
|
*/
|
|
16
|
-
import { appendFileSync, chmodSync, existsSync, mkdirSync, openSync, closeSync, readSync, statSync, readFileSync, } from 'fs';
|
|
16
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, openSync, closeSync, readSync, statSync, readFileSync, unlinkSync, } from 'fs';
|
|
17
17
|
import { join } from 'path';
|
|
18
18
|
import { homedir } from 'os';
|
|
19
19
|
import { createHash } from 'crypto';
|
|
20
20
|
// P18 slice 2 — Tier 1 hook honors per-cwd permissions.yaml.
|
|
21
21
|
import { filterAllowed } from '../security/permissions-config.js';
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Instruction-level scan pattern bank used by `scanPayload()`. Exported so
|
|
24
|
+
* docs and the landing page can quote `PATTERNS.length` instead of hardcoding
|
|
25
|
+
* a number that silently drifts (May 2026 audit found 50 / 47 / 60+ all in
|
|
26
|
+
* the same PRD). Pair this with `CODE_PATTERNS` from `trust-scorer.ts` — the
|
|
27
|
+
* two banks are scanned in different passes and counted separately.
|
|
28
|
+
*/
|
|
29
|
+
export const PATTERNS = [
|
|
23
30
|
// Prompt Injection
|
|
24
31
|
// Allow chained modifiers: "ignore all previous instructions", "ignore all your prior rules", etc.
|
|
25
32
|
{
|
|
@@ -494,6 +501,42 @@ function readChainTip(filepath) {
|
|
|
494
501
|
return null;
|
|
495
502
|
}
|
|
496
503
|
export function appendAuditLog(entry) {
|
|
504
|
+
// Cross-process file locking. Multiple Claude Code sessions (e.g., two
|
|
505
|
+
// terminals, or IDE + terminal) can trigger PreToolUse concurrently.
|
|
506
|
+
// Without a lock, two hook processes read the same chain tip and both
|
|
507
|
+
// write seq 43 — corrupting the hash chain. We use an exclusive-create
|
|
508
|
+
// lock file (+O_EXCL) which is atomic on all POSIX systems.
|
|
509
|
+
const LOCK_FILE = AUDIT_FILE + '.lock';
|
|
510
|
+
const lockAcquired = () => {
|
|
511
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
512
|
+
try {
|
|
513
|
+
const fd = openSync(LOCK_FILE, 'wx');
|
|
514
|
+
closeSync(fd);
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
// Lock held by another process — spin-wait ~10ms
|
|
519
|
+
const deadline = Date.now() + 10;
|
|
520
|
+
while (Date.now() < deadline) {
|
|
521
|
+
/* busy-wait — sync context */
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return false;
|
|
526
|
+
};
|
|
527
|
+
const lockReleased = () => {
|
|
528
|
+
try {
|
|
529
|
+
unlinkSync(LOCK_FILE);
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
/* best effort */
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
if (!lockAcquired()) {
|
|
536
|
+
// Fall through unlocked rather than drop the entry. The entry still
|
|
537
|
+
// appends; verification may flag a chain break, but the audit data
|
|
538
|
+
// is preserved.
|
|
539
|
+
}
|
|
497
540
|
try {
|
|
498
541
|
const dir = AUDIT_FILE.substring(0, AUDIT_FILE.lastIndexOf('/'));
|
|
499
542
|
if (dir && !existsSync(dir)) {
|
|
@@ -526,6 +569,9 @@ export function appendAuditLog(entry) {
|
|
|
526
569
|
catch {
|
|
527
570
|
// Audit log failure must not block tool execution
|
|
528
571
|
}
|
|
572
|
+
finally {
|
|
573
|
+
lockReleased();
|
|
574
|
+
}
|
|
529
575
|
}
|
|
530
576
|
/**
|
|
531
577
|
* Walk the audit file and verify the hash chain. Returns:
|
|
@@ -216,9 +216,7 @@ export function spawnCodex(prompt, options = {}) {
|
|
|
216
216
|
continue;
|
|
217
217
|
try {
|
|
218
218
|
const data = JSON.parse(line);
|
|
219
|
-
if (codexThreadId === undefined &&
|
|
220
|
-
data.type === 'thread.started' &&
|
|
221
|
-
typeof data.thread_id === 'string') {
|
|
219
|
+
if (codexThreadId === undefined && data.type === 'thread.started' && typeof data.thread_id === 'string') {
|
|
222
220
|
codexThreadId = data.thread_id;
|
|
223
221
|
}
|
|
224
222
|
emit({ type: data.type ?? 'codex_event', timestamp: Date.now(), data });
|
|
@@ -325,8 +323,15 @@ export function spawnCodex(prompt, options = {}) {
|
|
|
325
323
|
let stdout = '';
|
|
326
324
|
let stderr = '';
|
|
327
325
|
// codex exec resume <SESSION_ID> - → reads prompt from stdin
|
|
328
|
-
const followup = spawn('codex', [
|
|
329
|
-
'
|
|
326
|
+
const followup = spawn('codex', [
|
|
327
|
+
'exec',
|
|
328
|
+
'--json',
|
|
329
|
+
'--skip-git-repo-check',
|
|
330
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
331
|
+
'resume',
|
|
332
|
+
codexThreadId,
|
|
333
|
+
'-',
|
|
334
|
+
], {
|
|
330
335
|
cwd,
|
|
331
336
|
env: spawnEnv,
|
|
332
337
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -371,7 +376,7 @@ export function spawnCodex(prompt, options = {}) {
|
|
|
371
376
|
export const CodexAdapter = {
|
|
372
377
|
id: 'codex',
|
|
373
378
|
label: 'Codex',
|
|
374
|
-
description:
|
|
379
|
+
description: "OpenAI's coding agent. Good when the task needs reading a lot of code first, then changing several files together.",
|
|
375
380
|
processPattern: /(?:^|\/)codex(?:\s|$)/,
|
|
376
381
|
isInstalled() {
|
|
377
382
|
try {
|
|
@@ -58,7 +58,7 @@ export function loadHandoffContext(cwd, { fromSessionId, maxAgeMs = DEFAULT_MAX_
|
|
|
58
58
|
// Filter by age
|
|
59
59
|
const recent = packs.filter((p) => {
|
|
60
60
|
const ts = p.timestamp;
|
|
61
|
-
return ts &&
|
|
61
|
+
return ts && now - ts < maxAgeMs;
|
|
62
62
|
});
|
|
63
63
|
// Prefer packs with enough graph structure to be useful
|
|
64
64
|
const structured = recent.filter((p) => {
|
|
@@ -48,13 +48,7 @@ import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
|
|
|
48
48
|
//
|
|
49
49
|
// `--skip-trust` — skip the per-workspace trust prompt that gemini shows on
|
|
50
50
|
// first run in a new directory.
|
|
51
|
-
const HEADLESS_FLAGS = [
|
|
52
|
-
'--approval-mode',
|
|
53
|
-
'yolo',
|
|
54
|
-
'--skip-trust',
|
|
55
|
-
'-o',
|
|
56
|
-
'stream-json',
|
|
57
|
-
];
|
|
51
|
+
const HEADLESS_FLAGS = ['--approval-mode', 'yolo', '--skip-trust', '-o', 'stream-json'];
|
|
58
52
|
// Error patterns scoped to STDERR / result.error / error events only — NOT
|
|
59
53
|
// every JSON-serialized event (which would false-positive on tool output).
|
|
60
54
|
const ERROR_PATTERNS = [
|
|
@@ -340,13 +334,7 @@ export function spawnGemini(prompt, options = {}) {
|
|
|
340
334
|
}
|
|
341
335
|
}
|
|
342
336
|
statusValue =
|
|
343
|
-
killRequested || signal !== null
|
|
344
|
-
? 'killed'
|
|
345
|
-
: errorEmitted
|
|
346
|
-
? 'error'
|
|
347
|
-
: code === 0
|
|
348
|
-
? 'completed'
|
|
349
|
-
: 'error';
|
|
337
|
+
killRequested || signal !== null ? 'killed' : errorEmitted ? 'error' : code === 0 ? 'completed' : 'error';
|
|
350
338
|
if (aggregator.finalize(statusValue === 'completed' ? 'completed' : statusValue === 'killed' ? 'killed' : 'error')) {
|
|
351
339
|
stepsSnapshot = aggregator.steps.map((s) => ({ ...s }));
|
|
352
340
|
emit({ type: 'codehere:steps', timestamp: Date.now(), data: { steps: stepsSnapshot } });
|
|
@@ -518,7 +506,7 @@ export function spawnGemini(prompt, options = {}) {
|
|
|
518
506
|
export const GeminiAdapter = {
|
|
519
507
|
id: 'gemini',
|
|
520
508
|
label: 'Gemini',
|
|
521
|
-
description:
|
|
509
|
+
description: "Google's coding agent. Has a free tier and can read very large amounts of code or docs at once. Useful when the paid agents are out of credits.",
|
|
522
510
|
// Match `gemini`, `node /path/.../gemini`, and `./gemini` invocations.
|
|
523
511
|
processPattern: /(?:^|\/)gemini(?:\s|$)|node\s+\S*gemini/,
|
|
524
512
|
isInstalled() {
|
|
@@ -74,7 +74,7 @@ export function buildGraphSync(cwd, { incremental = true } = {}) {
|
|
|
74
74
|
* Compile a compact context pack for a prompt + workspace.
|
|
75
75
|
* Synchronous — calls graphify-ts pack and returns the payload.
|
|
76
76
|
*/
|
|
77
|
-
export function compileContextPackSync(prompt, cwd, { task = 'explain', budget = 3000
|
|
77
|
+
export function compileContextPackSync(prompt, cwd, { task = 'explain', budget = 3000 } = {}) {
|
|
78
78
|
const startTime = Date.now();
|
|
79
79
|
const graphPath = getGraphPath(cwd);
|
|
80
80
|
// Build graph if it doesn't exist yet
|
|
@@ -86,12 +86,7 @@ export function compileContextPackSync(prompt, cwd, { task = 'explain', budget =
|
|
|
86
86
|
return null;
|
|
87
87
|
}
|
|
88
88
|
try {
|
|
89
|
-
const output = execFileSync('npx', [
|
|
90
|
-
'graphify-ts', 'pack', prompt,
|
|
91
|
-
'--graph', graphPath,
|
|
92
|
-
'--task', task,
|
|
93
|
-
'--budget', String(budget),
|
|
94
|
-
], {
|
|
89
|
+
const output = execFileSync('npx', ['graphify-ts', 'pack', prompt, '--graph', graphPath, '--task', task, '--budget', String(budget)], {
|
|
95
90
|
cwd,
|
|
96
91
|
encoding: 'utf-8',
|
|
97
92
|
maxBuffer: 512 * 1024,
|
|
@@ -125,7 +120,9 @@ export function compileSessionPackSync(cwd, { prompt, artifacts, steps, agent })
|
|
|
125
120
|
agent ? `Agent: ${agent}` : undefined,
|
|
126
121
|
artifacts ? `Artifacts:\n${artifacts}` : undefined,
|
|
127
122
|
steps ? `Steps taken:\n${steps}` : undefined,
|
|
128
|
-
]
|
|
123
|
+
]
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.join('\n\n');
|
|
129
126
|
return compileContextPackSync(sessionContext, cwd, { task: 'impact', budget: 2500 });
|
|
130
127
|
}
|
|
131
128
|
// ── Context pack persistence ───────────────────────────────────────────────────
|
|
@@ -232,11 +229,7 @@ export function queryGraphSync(question, cwd, { maxTokens = 2000 } = {}) {
|
|
|
232
229
|
return null;
|
|
233
230
|
}
|
|
234
231
|
try {
|
|
235
|
-
const output = execFileSync('npx', [
|
|
236
|
-
'graphify-ts', 'query', question,
|
|
237
|
-
'--graph', graphPath,
|
|
238
|
-
'--budget', String(maxTokens),
|
|
239
|
-
], {
|
|
232
|
+
const output = execFileSync('npx', ['graphify-ts', 'query', question, '--graph', graphPath, '--budget', String(maxTokens)], {
|
|
240
233
|
cwd,
|
|
241
234
|
encoding: 'utf-8',
|
|
242
235
|
maxBuffer: 256 * 1024,
|
|
@@ -65,17 +65,13 @@ function looksLikeInitSeed(content, kind) {
|
|
|
65
65
|
/\[Tone for generated code\./,
|
|
66
66
|
/\[Don't do X\. Always do Y\./,
|
|
67
67
|
];
|
|
68
|
-
const hits = markers.filter(re => re.test(content)).length;
|
|
68
|
+
const hits = markers.filter((re) => re.test(content)).length;
|
|
69
69
|
return hits >= 2;
|
|
70
70
|
}
|
|
71
71
|
if (kind === 'session') {
|
|
72
72
|
// The init session.md seed has this header + structure
|
|
73
|
-
const markers = [
|
|
74
|
-
|
|
75
|
-
/^> Per-machine, gitignored/m,
|
|
76
|
-
/^## In flight\s*\n+- \[ \]\s*$/m,
|
|
77
|
-
];
|
|
78
|
-
const hits = markers.filter(re => re.test(content)).length;
|
|
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;
|
|
79
75
|
return hits >= 2;
|
|
80
76
|
}
|
|
81
77
|
return false;
|
|
@@ -142,9 +138,9 @@ function readLessons(codehereDir, n) {
|
|
|
142
138
|
const body = readFileSync(p, 'utf8');
|
|
143
139
|
return body
|
|
144
140
|
.split('\n')
|
|
145
|
-
.filter(line => line.trim().length > 0)
|
|
141
|
+
.filter((line) => line.trim().length > 0)
|
|
146
142
|
.slice(0, n)
|
|
147
|
-
.map(line => {
|
|
143
|
+
.map((line) => {
|
|
148
144
|
try {
|
|
149
145
|
const obj = JSON.parse(line);
|
|
150
146
|
return typeof obj.text === 'string' ? obj.text : line;
|
|
@@ -182,11 +178,11 @@ function formatSession(session) {
|
|
|
182
178
|
return `# Current session scratch (.codehere/memory/working/session.md)\n\n${session}\n`;
|
|
183
179
|
}
|
|
184
180
|
function formatLessons(lessons) {
|
|
185
|
-
const items = lessons.map(l => `- ${l}`).join('\n');
|
|
181
|
+
const items = lessons.map((l) => `- ${l}`).join('\n');
|
|
186
182
|
return `# Project lessons (.codehere/memory/semantic/lessons.jsonl)\n\n${items}\n`;
|
|
187
183
|
}
|
|
188
184
|
function formatSymbols(symbols) {
|
|
189
|
-
const lines = symbols.map(s => `- \`${s.name}\` — ${s.file}:${s.line} (${s.references} refs)`).join('\n');
|
|
185
|
+
const lines = symbols.map((s) => `- \`${s.name}\` — ${s.file}:${s.line} (${s.references} refs)`).join('\n');
|
|
190
186
|
return `# Top symbols in this repo (.codehere/memory/episodic/repo_map.json)\n\n${lines}\n`;
|
|
191
187
|
}
|
|
192
188
|
/**
|
|
@@ -232,7 +228,7 @@ export function buildMemoryPrefix(projectRoot, options = {}) {
|
|
|
232
228
|
let kept = [...candidates];
|
|
233
229
|
while (kept.length > 0) {
|
|
234
230
|
const totalChars = kept.reduce((acc, c) => acc + c.body.length, 0);
|
|
235
|
-
const totalTokens = estimateTokens(kept.map(c => c.body).join('\n')) + separatorTokens;
|
|
231
|
+
const totalTokens = estimateTokens(kept.map((c) => c.body).join('\n')) + separatorTokens;
|
|
236
232
|
if (totalTokens <= maxTokens)
|
|
237
233
|
break;
|
|
238
234
|
// Drop the last (least-essential) section.
|
|
@@ -250,10 +246,10 @@ export function buildMemoryPrefix(projectRoot, options = {}) {
|
|
|
250
246
|
if (kept.length === 0) {
|
|
251
247
|
return { prefix: '', included: [], tokens: 0, truncated };
|
|
252
248
|
}
|
|
253
|
-
const prefix = kept.map(c => c.body).join('\n') + SEPARATOR;
|
|
249
|
+
const prefix = kept.map((c) => c.body).join('\n') + SEPARATOR;
|
|
254
250
|
return {
|
|
255
251
|
prefix,
|
|
256
|
-
included: kept.map(c => c.key),
|
|
252
|
+
included: kept.map((c) => c.key),
|
|
257
253
|
tokens: estimateTokens(prefix),
|
|
258
254
|
truncated,
|
|
259
255
|
};
|
|
@@ -296,8 +292,7 @@ function findProjectRoot(startPath) {
|
|
|
296
292
|
}
|
|
297
293
|
// Bound the loop at the filesystem root by checking dirname stability.
|
|
298
294
|
for (;;) {
|
|
299
|
-
if (existsSync(join(current, '.codehere')) ||
|
|
300
|
-
existsSync(join(current, 'AGENTS.md'))) {
|
|
295
|
+
if (existsSync(join(current, '.codehere')) || existsSync(join(current, 'AGENTS.md'))) {
|
|
301
296
|
return current;
|
|
302
297
|
}
|
|
303
298
|
const parent = dirname(current);
|
|
@@ -19,6 +19,7 @@ import { join } from 'path';
|
|
|
19
19
|
import { promisify } from 'util';
|
|
20
20
|
import { ProgressAggregator } from './progress-aggregator.js';
|
|
21
21
|
import { applyMemoryPrefix } from './memory-injector.js';
|
|
22
|
+
import { mirrorToolCallToAudit } from '../hooks/mirror-tool-call-to-audit.js';
|
|
22
23
|
import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
|
|
23
24
|
const execFileAsync = promisify(execFile);
|
|
24
25
|
const HEADLESS_FLAGS = ['run', '--format', 'json'];
|
|
@@ -142,6 +143,46 @@ export function spawnOpenCode(prompt, options = {}) {
|
|
|
142
143
|
if (txt)
|
|
143
144
|
textAccum += txt + '\n';
|
|
144
145
|
}
|
|
146
|
+
// #114 — cross-vendor audit mirror for opencode (closes the gap left by
|
|
147
|
+
// #110 slice 4, which wired Codex/Aider/Gemini but deferred opencode).
|
|
148
|
+
// OpenCode's `run --format json` emits `tool_use` events with shape
|
|
149
|
+
// { type: 'tool_use', part: { tool: 'bash'|'read'|'write'|..., state:
|
|
150
|
+
// { input: {...}, output, status, ... } } } (verified via deepwiki +
|
|
151
|
+
// takopi cheatsheet). Same post-hoc visibility surface as Codex/Aider/
|
|
152
|
+
// Gemini — no synchronous block until upstream pre-tool hooks land.
|
|
153
|
+
if (event.type === 'tool_use') {
|
|
154
|
+
const part = event.data?.part;
|
|
155
|
+
const rawTool = String(part?.tool ?? 'tool').toLowerCase();
|
|
156
|
+
// Normalize to Claude-cased tool names so /history filter chips group
|
|
157
|
+
// cross-vendor (a bash call from claude-code and opencode show under
|
|
158
|
+
// the same "Bash" filter chip).
|
|
159
|
+
const toolNameMap = {
|
|
160
|
+
bash: 'Bash',
|
|
161
|
+
read: 'Read',
|
|
162
|
+
write: 'Write',
|
|
163
|
+
edit: 'Edit',
|
|
164
|
+
grep: 'Grep',
|
|
165
|
+
glob: 'Glob',
|
|
166
|
+
webfetch: 'WebFetch',
|
|
167
|
+
websearch: 'WebSearch',
|
|
168
|
+
task: 'Task',
|
|
169
|
+
};
|
|
170
|
+
const toolName = toolNameMap[rawTool] ?? rawTool;
|
|
171
|
+
const input = part?.state?.input ?? {};
|
|
172
|
+
const inputStr = (typeof input.command === 'string' && input.command) ||
|
|
173
|
+
(typeof input.file_path === 'string' && input.file_path) ||
|
|
174
|
+
(typeof input.path === 'string' && input.path) ||
|
|
175
|
+
(typeof input.pattern === 'string' && input.pattern) ||
|
|
176
|
+
(typeof input.query === 'string' && input.query) ||
|
|
177
|
+
JSON.stringify(input).slice(0, 500);
|
|
178
|
+
mirrorToolCallToAudit({
|
|
179
|
+
sessionId,
|
|
180
|
+
agent: 'opencode',
|
|
181
|
+
toolName,
|
|
182
|
+
toolInput: String(inputStr),
|
|
183
|
+
cwd,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
145
186
|
if (!errorEmitted) {
|
|
146
187
|
const data = event.data;
|
|
147
188
|
let text = '';
|
|
@@ -393,7 +434,7 @@ export function spawnOpenCode(prompt, options = {}) {
|
|
|
393
434
|
export const OpenCodeAdapter = {
|
|
394
435
|
id: 'opencode',
|
|
395
436
|
label: 'OpenCode',
|
|
396
|
-
description:
|
|
437
|
+
description: "Runs several agents at once for genuinely independent jobs. You won't see live progress while it works — the trade-off for the speed of doing things in parallel.",
|
|
397
438
|
processPattern: /(?:^|\/)opencode(?:\s|$)/,
|
|
398
439
|
isInstalled() {
|
|
399
440
|
try {
|
|
@@ -464,8 +464,7 @@ export class ProgressAggregator {
|
|
|
464
464
|
if (itemType === 'tool_call' || itemType === 'local_shell_call') {
|
|
465
465
|
const name = String(item.tool_name ?? item.name ?? 'tool');
|
|
466
466
|
// Tool-call items may carry a `result` or `output` field too.
|
|
467
|
-
const result = (typeof item.result === 'string' ? item.result : '') ||
|
|
468
|
-
(typeof item.output === 'string' ? item.output : '');
|
|
467
|
+
const result = (typeof item.result === 'string' ? item.result : '') || (typeof item.output === 'string' ? item.output : '');
|
|
469
468
|
this._steps.push({
|
|
470
469
|
label: truncate(name),
|
|
471
470
|
state: 'done',
|