codehere 0.7.1 → 1.0.1
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 +22 -13
- package/dist/application/services/verdict-service.js +154 -0
- package/dist/application/use-cases/assess-cross-vendor.js +126 -0
- package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
- package/dist/application/use-cases/export-aat.js +107 -0
- package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
- package/dist/application/use-cases/generate-final-report.js +140 -15
- package/dist/application/use-cases/generate-narration.js +5 -3
- package/dist/application/use-cases/generate-thread-title.js +5 -2
- package/dist/application/use-cases/narrator-prompt-template.js +1 -1
- package/dist/application/use-cases/plan-auto-review.js +57 -0
- package/dist/application/use-cases/reconcile-audit.js +100 -0
- package/dist/application/use-cases/rerun-tests.js +226 -0
- package/dist/application/use-cases/search-sessions.js +70 -0
- package/dist/application/use-cases/should-auto-review.js +71 -0
- package/dist/application/use-cases/thread-export.js +90 -0
- package/dist/application/use-cases/verify-claim.js +187 -5
- package/dist/domain/audit.js +11 -0
- package/dist/domain/services/codebase-detector.js +0 -6
- package/dist/domain/task.js +91 -0
- package/dist/error-handler.js +3 -115
- package/dist/index.js +279 -31
- package/dist/infrastructure/completion/bash-completion.js +7 -3
- package/dist/infrastructure/completion/fish-completion.js +7 -12
- package/dist/infrastructure/completion/zsh-completion.js +7 -12
- package/dist/infrastructure/config/config-manager.js +4 -3
- package/dist/infrastructure/config/product-context.js +1 -19
- package/dist/infrastructure/context/adapters/figma-source.js +37 -2
- package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
- package/dist/infrastructure/context/github-api.js +110 -0
- package/dist/infrastructure/context/github-device-flow.js +65 -0
- package/dist/infrastructure/context/oauth-store.js +5 -2
- package/dist/infrastructure/context/registry-bootstrap.js +0 -4
- package/dist/infrastructure/cost/budget-config.js +4 -3
- package/dist/infrastructure/cost/cost-router.js +16 -23
- package/dist/infrastructure/cost/spend-aggregator.js +91 -16
- package/dist/infrastructure/cost/team-ledger.js +355 -0
- package/dist/infrastructure/discovery/discovery-log.js +85 -0
- package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +139 -3
- package/dist/infrastructure/hooks/hook-mode.js +20 -4
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
- package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
- package/dist/infrastructure/observability/execution-tracer.js +1 -1
- package/dist/infrastructure/orchestration/agents-config-seed.js +23 -11
- package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
- package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
- package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
- package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
- package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
- package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
- package/dist/infrastructure/orchestration/context-handoff.js +32 -14
- package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
- package/dist/infrastructure/orchestration/failover-config.js +1 -11
- package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +134 -3
- package/dist/infrastructure/orchestration/graphify-context.js +14 -59
- package/dist/infrastructure/orchestration/memory-injector.js +4 -1
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +167 -9
- package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
- package/dist/infrastructure/orchestration/progress-aggregator.js +275 -23
- package/dist/infrastructure/orchestration/session-manager.js +593 -94
- package/dist/infrastructure/orchestration/session-persistence.js +177 -41
- package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
- package/dist/infrastructure/protocols/cloud-sync.js +2 -2
- package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +1498 -354
- package/dist/infrastructure/protocols/mcp-tools.js +401 -0
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
- package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
- package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
- package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
- package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
- package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
- package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
- package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
- package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
- package/dist/infrastructure/protocols/ui/styles.js +2147 -0
- package/dist/infrastructure/protocols/ui/tokens.js +96 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +116 -4126
- package/dist/infrastructure/protocols/verify-page.js +884 -148
- package/dist/infrastructure/security/fix-code.js +144 -0
- package/dist/infrastructure/security/fix-secret.js +89 -0
- package/dist/infrastructure/security/permissions-config.js +3 -2
- package/dist/infrastructure/security/scan-types.js +10 -0
- package/dist/infrastructure/security/ship-scan.js +195 -0
- package/dist/infrastructure/security/trust-scorer.js +70 -3
- package/dist/infrastructure/security/vibe-checks.js +99 -0
- package/dist/infrastructure/storage/atomic-write.js +86 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +151 -4
- package/dist/infrastructure/storage/project-thread-reader.js +262 -25
- package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
- package/dist/monitoring.js +2 -2
- package/dist/presentation/cli/commands/audit-command.js +79 -4
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/check-command.js +96 -0
- package/dist/presentation/cli/commands/context-source-command.js +20 -1
- package/dist/presentation/cli/commands/help-command.js +5 -5
- package/dist/presentation/cli/commands/hook-command.js +5 -5
- package/dist/presentation/cli/commands/init-command.js +89 -66
- package/dist/presentation/cli/commands/memory-command.js +28 -28
- package/dist/presentation/cli/commands/permission-command.js +1 -12
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/protocols-command.js +4 -4
- package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
- package/dist/presentation/cli/commands/report-command.js +48 -32
- package/dist/presentation/cli/commands/scan-command.js +52 -0
- package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
- package/dist/presentation/cli/commands/search-command.js +51 -0
- package/dist/presentation/cli/commands/setup-command.js +31 -8
- package/dist/presentation/cli/commands/status-command.js +39 -86
- package/dist/presentation/cli/commands/team-command.js +256 -0
- package/dist/presentation/cli/commands/thread-command.js +87 -5
- package/dist/presentation/cli/commands/trust-command.js +53 -31
- package/dist/presentation/cli/commands/verify-command.js +74 -9
- package/dist/presentation/cli/error-display.js +0 -8
- package/dist/presentation/cli/repl.js +15 -7
- package/dist/presentation/cli/task-deliverable.js +36 -10
- package/dist/testing/replay-adapter.js +0 -19
- package/dist/ui.js +51 -239
- package/dist/utils/version.js +0 -6
- package/package.json +30 -10
- package/dist/domain/entities/task.js +0 -61
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/task-repository.js +0 -206
|
@@ -34,11 +34,11 @@ import { promisify } from 'util';
|
|
|
34
34
|
const execFileAsync = promisify(execFile);
|
|
35
35
|
import { EventEmitter } from 'events';
|
|
36
36
|
import { randomUUID } from 'crypto';
|
|
37
|
-
import { existsSync } from 'fs';
|
|
37
|
+
import { existsSync, readFileSync } from 'fs';
|
|
38
38
|
import { homedir } from 'os';
|
|
39
39
|
import { join } from 'path';
|
|
40
40
|
import { ProgressAggregator } from './progress-aggregator.js';
|
|
41
|
-
import { resolveAgentSpawn } from './mocks/resolve-agent-spawn.js';
|
|
41
|
+
import { resolveAgentSpawn, isMockMode } from './mocks/resolve-agent-spawn.js';
|
|
42
42
|
import { applyMemoryPrefix } from './memory-injector.js';
|
|
43
43
|
import { mirrorToolCallToAudit } from '../hooks/mirror-tool-call-to-audit.js';
|
|
44
44
|
import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
|
|
@@ -78,8 +78,26 @@ const ERROR_PATTERNS = [
|
|
|
78
78
|
message: 'Google AI Studio / Vertex quota exhausted.',
|
|
79
79
|
recovery: 'Check quota at https://aistudio.google.com/ or top up your Google Cloud billing account.',
|
|
80
80
|
},
|
|
81
|
+
{
|
|
82
|
+
// BUG-7 · the CLI refuses headless runs in untrusted folders. codehere sets
|
|
83
|
+
// GEMINI_CLI_TRUST_WORKSPACE=true at spawn (same rationale as yolo/sandbox:
|
|
84
|
+
// codehere's own hook is the gate), so this fires only if that stops working.
|
|
85
|
+
code: 'workspace_untrusted',
|
|
86
|
+
regex: /not running in a trusted directory|trusted.folders|GEMINI_CLI_TRUST_WORKSPACE/i,
|
|
87
|
+
message: 'Gemini refused to run in this folder (workspace not trusted).',
|
|
88
|
+
recovery: 'Run the task from a folder gemini trusts, or trust it once interactively: open `gemini` in that folder and accept the trust prompt.',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
// Observed live 2026-07-18: Google discontinued the free "Code Assist for
|
|
92
|
+
// individuals" tier for this CLI. Without this pattern the user sees a
|
|
93
|
+
// generic "network hiccup" guess for a permanent, actionable condition.
|
|
94
|
+
code: 'tier_discontinued',
|
|
95
|
+
regex: /IneligibleTierError|no longer supported for Gemini Code Assist/i,
|
|
96
|
+
message: "Google discontinued this Gemini CLI's free tier.",
|
|
97
|
+
recovery: 'Set GEMINI_API_KEY (free key at https://aistudio.google.com/apikey) or migrate per https://antigravity.google — then rerun.',
|
|
98
|
+
},
|
|
81
99
|
];
|
|
82
|
-
function scanForKnownError(text) {
|
|
100
|
+
export function scanForKnownError(text) {
|
|
83
101
|
for (const p of ERROR_PATTERNS) {
|
|
84
102
|
if (p.regex.test(text))
|
|
85
103
|
return { code: p.code, message: p.message, recovery: p.recovery };
|
|
@@ -138,6 +156,7 @@ export function spawnGemini(prompt, options = {}) {
|
|
|
138
156
|
let artifactsSnapshot;
|
|
139
157
|
let textAccum = '';
|
|
140
158
|
let usageSnapshot;
|
|
159
|
+
let geminiModel; // real model from the init event → per-model cost
|
|
141
160
|
let errorEmitted = false;
|
|
142
161
|
const emit = (event) => {
|
|
143
162
|
buffer.push(event);
|
|
@@ -202,6 +221,7 @@ export function spawnGemini(prompt, options = {}) {
|
|
|
202
221
|
inputTokens: Number(s.input_tokens ?? 0),
|
|
203
222
|
outputTokens: Number(s.output_tokens ?? 0),
|
|
204
223
|
...(s.cached ? { cacheReadTokens: Number(s.cached) } : {}),
|
|
224
|
+
...(geminiModel ? { model: geminiModel } : {}),
|
|
205
225
|
};
|
|
206
226
|
}
|
|
207
227
|
}
|
|
@@ -245,6 +265,10 @@ export function spawnGemini(prompt, options = {}) {
|
|
|
245
265
|
const spawnEnv = {
|
|
246
266
|
...process.env,
|
|
247
267
|
GEMINI_SANDBOX: 'false',
|
|
268
|
+
// BUG-7 · the CLI's trusted-folder prompt blocks headless spawns (observed
|
|
269
|
+
// live: task died as "error" with a misattributed cause). Same rationale as
|
|
270
|
+
// yolo + sandbox above: codehere's safety hook is the external gate.
|
|
271
|
+
GEMINI_CLI_TRUST_WORKSPACE: 'true',
|
|
248
272
|
...(options.env ?? {}),
|
|
249
273
|
};
|
|
250
274
|
let child;
|
|
@@ -312,6 +336,9 @@ export function spawnGemini(prompt, options = {}) {
|
|
|
312
336
|
const data = JSON.parse(line);
|
|
313
337
|
if (data.type === 'init' && typeof data.session_id === 'string') {
|
|
314
338
|
geminiSessionUuid = data.session_id;
|
|
339
|
+
const im = data.model;
|
|
340
|
+
if (typeof im === 'string' && im)
|
|
341
|
+
geminiModel = im; // per-model cost
|
|
315
342
|
}
|
|
316
343
|
emit({ type: data.type ?? 'gemini_event', timestamp: Date.now(), data });
|
|
317
344
|
}
|
|
@@ -455,6 +482,10 @@ export function spawnGemini(prompt, options = {}) {
|
|
|
455
482
|
// 15s = 5s upstream LLM cap + parse/flush/spawn overhead + slack.
|
|
456
483
|
// Override via CODEHERE_GEMINI_LIST_TIMEOUT_MS for slow boxes.
|
|
457
484
|
const timeoutMs = Number(process.env.CODEHERE_GEMINI_LIST_TIMEOUT_MS) || 15000;
|
|
485
|
+
// S9 · never execute the real binary under mock — the resume-index
|
|
486
|
+
// refinement is meaningless against the mock REPL anyway.
|
|
487
|
+
if (isMockMode())
|
|
488
|
+
throw new Error('mock mode: skip real --list-sessions');
|
|
458
489
|
const { stdout: listOut } = await execFileAsync('gemini', ['--list-sessions'], {
|
|
459
490
|
cwd,
|
|
460
491
|
env: spawnEnv,
|
|
@@ -472,7 +503,11 @@ export function spawnGemini(prompt, options = {}) {
|
|
|
472
503
|
return new Promise((resolve, reject) => {
|
|
473
504
|
let stdout = '';
|
|
474
505
|
let stderr = '';
|
|
475
|
-
|
|
506
|
+
// S9 · route follow-ups through the mock seam too — under
|
|
507
|
+
// CODEHERE_MOCK=1 this hits the mock REPL, not real gemini.
|
|
508
|
+
// (Was a real-spend leak on reply; mirrors the codex fix.)
|
|
509
|
+
const sp2 = resolveAgentSpawn('gemini', 'gemini', ['-p', text, '--resume', resumeArg, ...HEADLESS_FLAGS], text);
|
|
510
|
+
const followup = spawn(sp2.command, sp2.args, {
|
|
476
511
|
cwd,
|
|
477
512
|
env: spawnEnv,
|
|
478
513
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -516,7 +551,7 @@ export function spawnGemini(prompt, options = {}) {
|
|
|
516
551
|
export const GeminiAdapter = {
|
|
517
552
|
id: 'gemini',
|
|
518
553
|
label: 'Gemini',
|
|
519
|
-
description: "Google's coding agent.
|
|
554
|
+
description: "Google's coding agent. Free with an AI Studio API key, and can read very large amounts of code or docs at once. Useful when the paid agents are out of credits.",
|
|
520
555
|
// Match `gemini`, `node /path/.../gemini`, and `./gemini` invocations.
|
|
521
556
|
processPattern: /(?:^|\/)gemini(?:\s|$)|node\s+\S*gemini/,
|
|
522
557
|
isInstalled() {
|
|
@@ -557,14 +592,59 @@ export const GeminiAdapter = {
|
|
|
557
592
|
// the task errored with no clear next step. Surfaced May 1 2026 by the
|
|
558
593
|
// multi-agent dogfood test (see SECURITY_AUDIT_2026-05-01.md spirit —
|
|
559
594
|
// honest readiness signals are a UX security property).
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const
|
|
566
|
-
|
|
595
|
+
// Validate the VALUE, not just presence. `!!process.env.X` is true even when
|
|
596
|
+
// a var is set to "" / "false" / "0" — which is exactly how readiness lied
|
|
597
|
+
// "ready" for a gemini that cannot auth. A key must look like a key; the
|
|
598
|
+
// USE_VERTEXAI / USE_GCA flags must be truthy strings; a credentials path
|
|
599
|
+
// must point to a file that exists.
|
|
600
|
+
const isKey = (v) => typeof v === 'string' && v.trim().length >= 8;
|
|
601
|
+
const isTrue = (v) => v === '1' || (typeof v === 'string' && v.trim().toLowerCase() === 'true');
|
|
602
|
+
const isCredFile = (v) => typeof v === 'string' && v.trim().length > 0 && existsSync(v.trim());
|
|
603
|
+
const hasEnvAuth = isKey(process.env.GEMINI_API_KEY) ||
|
|
604
|
+
isKey(process.env.GOOGLE_API_KEY) ||
|
|
605
|
+
isTrue(process.env.GOOGLE_GENAI_USE_VERTEXAI) ||
|
|
606
|
+
isTrue(process.env.GOOGLE_GENAI_USE_GCA) ||
|
|
607
|
+
isCredFile(process.env.GOOGLE_APPLICATION_CREDENTIALS);
|
|
608
|
+
// A settings.json that EXISTS but has no configured auth method is not auth —
|
|
609
|
+
// gemini's own error ("set an Auth method in settings.json") proves it. Require
|
|
610
|
+
// a non-empty selectedAuthType (top-level or under security.auth). oauth_creds
|
|
611
|
+
// existing IS real cached auth.
|
|
612
|
+
const settingsAuthType = () => {
|
|
613
|
+
try {
|
|
614
|
+
const p = join(homedir(), '.gemini', 'settings.json');
|
|
615
|
+
if (!existsSync(p))
|
|
616
|
+
return '';
|
|
617
|
+
const s = JSON.parse(readFileSync(p, 'utf8'));
|
|
618
|
+
const t = s.selectedAuthType ?? s.security?.auth?.selectedType;
|
|
619
|
+
return typeof t === 'string' ? t.trim() : '';
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
return '';
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
// 2026-07: Google retired the individual OAuth tier ("Gemini Code Assist for
|
|
626
|
+
// individuals" moved into Antigravity). A cached personal login now gets a
|
|
627
|
+
// server-side IneligibleTierError — observed live on this exact config
|
|
628
|
+
// (selectedType oauth-personal + oauth_creds.json present, latest CLI 0.49.0).
|
|
629
|
+
// So oauth-personal, with or without cached creds, is NOT auth anymore.
|
|
630
|
+
// Other selected types (Vertex, enterprise) keep working and stay accepted.
|
|
631
|
+
const authType = settingsAuthType();
|
|
632
|
+
const personalOauthDead = authType === 'oauth-personal' || (!authType && existsSync(join(homedir(), '.gemini', 'oauth_creds.json')));
|
|
633
|
+
const hasFileAuth = !!authType && authType !== 'oauth-personal';
|
|
567
634
|
if (!hasEnvAuth && !hasFileAuth) {
|
|
635
|
+
if (personalOauthDead) {
|
|
636
|
+
return {
|
|
637
|
+
id: this.id,
|
|
638
|
+
label: this.label,
|
|
639
|
+
state: 'needs_auth',
|
|
640
|
+
installed: true,
|
|
641
|
+
authHint: {
|
|
642
|
+
command: 'export GEMINI_API_KEY=... # free key: https://aistudio.google.com/apikey',
|
|
643
|
+
url: 'https://aistudio.google.com/apikey',
|
|
644
|
+
detail: 'Your Google login uses the retired individual OAuth tier (Google moved it into Antigravity, July 2026), so the server now refuses it. Set GEMINI_API_KEY with a free key from https://aistudio.google.com/apikey to restore gemini.',
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
}
|
|
568
648
|
return {
|
|
569
649
|
id: this.id,
|
|
570
650
|
label: this.label,
|
|
@@ -36,10 +36,12 @@ import { homedir } from 'os';
|
|
|
36
36
|
import { join } from 'path';
|
|
37
37
|
import { randomUUID } from 'crypto';
|
|
38
38
|
import { ProgressAggregator } from './progress-aggregator.js';
|
|
39
|
+
import { resolveAgentSpawn } from './mocks/resolve-agent-spawn.js';
|
|
39
40
|
import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
|
|
40
41
|
import { applyMemoryPrefix } from './memory-injector.js';
|
|
41
|
-
|
|
42
|
-
const
|
|
42
|
+
import { mirrorToolCallToAudit } from '../hooks/mirror-tool-call-to-audit.js';
|
|
43
|
+
const CONFIG_PATH = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'agents.yaml');
|
|
44
|
+
const CONFIG_PATH_JSON = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'agents.json');
|
|
43
45
|
/** Minimal YAML parser for the narrow shape above. Intentionally NOT a full YAML
|
|
44
46
|
* parser — avoids dragging in a dep. Handles: `key: value`, lists with `- key: val`,
|
|
45
47
|
* nested lists of simple strings via `[a, b]`, comments after `#`. Errors loud.
|
|
@@ -127,6 +129,74 @@ export function loadAgentsConfig() {
|
|
|
127
129
|
}
|
|
128
130
|
return [];
|
|
129
131
|
}
|
|
132
|
+
// Minimal generic failover fingerprints. Config-driven CLIs have no
|
|
133
|
+
// per-provider error bank (we don't know the vendor), so detect only the two
|
|
134
|
+
// signals every provider phrases recognizably: rate-limit and auth. Emits the
|
|
135
|
+
// same `codehere:error` { code, message, recovery } shape the built-in adapters
|
|
136
|
+
// emit — the session-manager's failover classifier consumes that shape and
|
|
137
|
+
// treats 'rate_limit' as failover-eligible ('auth_invalid' is surfaced only).
|
|
138
|
+
const ERROR_PATTERNS = [
|
|
139
|
+
{
|
|
140
|
+
code: 'rate_limit',
|
|
141
|
+
regex: /rate.?limit|429|quota/i,
|
|
142
|
+
message: 'The agent was rate-limited by its provider.',
|
|
143
|
+
recovery: 'Wait a minute and retry, or switch the task to a different agent.',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
code: 'auth_invalid',
|
|
147
|
+
regex: /auth|401|403|api.?key|login/i,
|
|
148
|
+
message: "The agent's auth was rejected or missing.",
|
|
149
|
+
recovery: 'Check the API key / login for this CLI, re-authenticate, and retry.',
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
/** @internal — exported for tests; not part of the public API. */
|
|
153
|
+
export function scanForKnownError(text) {
|
|
154
|
+
for (const p of ERROR_PATTERNS) {
|
|
155
|
+
if (p.regex.test(text))
|
|
156
|
+
return { code: p.code, message: p.message, recovery: p.recovery };
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Normalize a Claude-Code-style stream-json event (cursor-agent et al.) into the
|
|
162
|
+
* flat event shapes the spawn handler already understands. Returns null for
|
|
163
|
+
* events that need no translation (the caller emits them verbatim).
|
|
164
|
+
*
|
|
165
|
+
* - `{ type:'tool_call', subtype:'started', tool_call:{ <kind>ToolCall:{ args } } }`
|
|
166
|
+
* → `{ type:'tool_use', data:{ name:<kind>, input:args } }` so the existing
|
|
167
|
+
* mirror lands the call in the audit chain (only 'started', so a call is
|
|
168
|
+
* counted once, not twice with 'completed').
|
|
169
|
+
* - `{ type:'assistant', message:{ content:[{type:'text',text}] } }`
|
|
170
|
+
* → `{ type:'stdout', data:{ line:<text> } }` so the agent's findings feed
|
|
171
|
+
* textAccum + the Final Report (else a read-only review records no findings).
|
|
172
|
+
*
|
|
173
|
+
* @internal — exported for tests; not part of the public API.
|
|
174
|
+
*/
|
|
175
|
+
export function normalizeStreamJson(data) {
|
|
176
|
+
if (data.type === 'tool_call' && data.subtype === 'started' && data.tool_call) {
|
|
177
|
+
const tc = data.tool_call;
|
|
178
|
+
const kind = Object.keys(tc).find((k) => k.endsWith('ToolCall'));
|
|
179
|
+
if (kind) {
|
|
180
|
+
const call = tc[kind];
|
|
181
|
+
return {
|
|
182
|
+
type: 'tool_use',
|
|
183
|
+
timestamp: Date.now(),
|
|
184
|
+
data: { name: kind.replace(/ToolCall$/, ''), input: call?.args ?? {} },
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (data.type === 'assistant') {
|
|
189
|
+
const content = data.message?.content ?? [];
|
|
190
|
+
const text = content
|
|
191
|
+
.filter((c) => c.type === 'text' && typeof c.text === 'string')
|
|
192
|
+
.map((c) => c.text)
|
|
193
|
+
.join('\n')
|
|
194
|
+
.trim();
|
|
195
|
+
if (text)
|
|
196
|
+
return { type: 'stdout', timestamp: Date.now(), data: { line: text } };
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
130
200
|
function spawnGeneric(cfg, prompt, options = {}) {
|
|
131
201
|
const sessionId = randomUUID();
|
|
132
202
|
const startedAt = Date.now();
|
|
@@ -146,6 +216,12 @@ function spawnGeneric(cfg, prompt, options = {}) {
|
|
|
146
216
|
let stepsSnapshot = [];
|
|
147
217
|
let artifactsSnapshot;
|
|
148
218
|
let textAccum = '';
|
|
219
|
+
// Bounded recent-stdout tail for error scanning (same posture as aider):
|
|
220
|
+
// scanning the whole accumulated buffer lets distant benign prose combine
|
|
221
|
+
// into false matches; a single line misses wrapped error bodies.
|
|
222
|
+
let scanTail = '';
|
|
223
|
+
const SCAN_TAIL_CHARS = 600;
|
|
224
|
+
let errorEmitted = false;
|
|
149
225
|
const emit = (event) => {
|
|
150
226
|
buffer.push(event);
|
|
151
227
|
emitter.emit('event', event);
|
|
@@ -158,6 +234,45 @@ function spawnGeneric(cfg, prompt, options = {}) {
|
|
|
158
234
|
if (event.type === 'stdout' && event.data?.line) {
|
|
159
235
|
textAccum += String(event.data.line) + '\n';
|
|
160
236
|
}
|
|
237
|
+
// Audit-trail parity: generic CLIs that stream jsonl tool_use events
|
|
238
|
+
// (gemini-style { tool_name, parameters } or claude-style { name, input })
|
|
239
|
+
// get the same post-hoc mirror the built-in adapters fire — their tool
|
|
240
|
+
// calls land in the hash-chained audit instead of vanishing.
|
|
241
|
+
if (event.type === 'tool_use') {
|
|
242
|
+
const data = event.data;
|
|
243
|
+
const toolName = String(data?.tool_name ?? data?.name ?? 'tool');
|
|
244
|
+
const params = data?.parameters ?? data?.input ?? {};
|
|
245
|
+
const input = (typeof params.command === 'string' && params.command) ||
|
|
246
|
+
(typeof params.file_path === 'string' && params.file_path) ||
|
|
247
|
+
(typeof params.path === 'string' && params.path) ||
|
|
248
|
+
(typeof params.query === 'string' && params.query) ||
|
|
249
|
+
JSON.stringify(params).slice(0, 500);
|
|
250
|
+
mirrorToolCallToAudit({ sessionId, agent: cfg.id, toolName, toolInput: String(input), cwd });
|
|
251
|
+
}
|
|
252
|
+
if (!errorEmitted) {
|
|
253
|
+
const data = event.data;
|
|
254
|
+
let text = '';
|
|
255
|
+
if (event.type === 'stderr' && typeof data?.line === 'string') {
|
|
256
|
+
text = data.line;
|
|
257
|
+
}
|
|
258
|
+
else if (event.type === 'stdout' && typeof data?.line === 'string') {
|
|
259
|
+
scanTail = (scanTail + '\n' + data.line).slice(-SCAN_TAIL_CHARS);
|
|
260
|
+
text = scanTail;
|
|
261
|
+
}
|
|
262
|
+
if (text) {
|
|
263
|
+
const hit = scanForKnownError(text);
|
|
264
|
+
if (hit) {
|
|
265
|
+
errorEmitted = true;
|
|
266
|
+
const errEvent = {
|
|
267
|
+
type: 'codehere:error',
|
|
268
|
+
timestamp: Date.now(),
|
|
269
|
+
data: { code: hit.code, message: hit.message, recovery: hit.recovery },
|
|
270
|
+
};
|
|
271
|
+
buffer.push(errEvent);
|
|
272
|
+
emitter.emit('event', errEvent);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
161
276
|
};
|
|
162
277
|
const spawnEnv = { ...process.env, ...(cfg.env ?? {}), ...(options.env ?? {}) };
|
|
163
278
|
const usesStdin = cfg.prompt_via === 'stdin';
|
|
@@ -172,8 +287,13 @@ function spawnGeneric(cfg, prompt, options = {}) {
|
|
|
172
287
|
// The `cfg.command` path is from a user-edited YAML and is therefore
|
|
173
288
|
// user-controlled; the user choosing what binary to launch on their own
|
|
174
289
|
// machine is documented + accepted in CODEHERE.md threat-model section.
|
|
290
|
+
//
|
|
291
|
+
// Routed through the mock seam like every built-in adapter — under
|
|
292
|
+
// CODEHERE_MOCK=1 spawning the configured binary directly would execute
|
|
293
|
+
// the real CLI (the real-spend leak class).
|
|
294
|
+
const sp = resolveAgentSpawn(cfg.id, cfg.command, [...argv, ...(options.extraArgs ?? [])], finalPrompt);
|
|
175
295
|
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
176
|
-
child = spawn(
|
|
296
|
+
child = spawn(sp.command, sp.args, {
|
|
177
297
|
cwd,
|
|
178
298
|
env: spawnEnv,
|
|
179
299
|
stdio: [usesStdin ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
@@ -224,6 +344,17 @@ function spawnGeneric(cfg, prompt, options = {}) {
|
|
|
224
344
|
if (cfg.format === 'jsonl') {
|
|
225
345
|
try {
|
|
226
346
|
const data = JSON.parse(line);
|
|
347
|
+
// Claude-Code-style stream-json (cursor-agent, etc.) nests tool calls
|
|
348
|
+
// and findings differently from the flat { tool_use } shape. Normalize
|
|
349
|
+
// them into the events the handlers already understand, so a CLI agent's
|
|
350
|
+
// reads land in the audit chain (attribution) and its findings feed the
|
|
351
|
+
// Final Report — without that, a cross-vendor review reads "couldn't
|
|
352
|
+
// attribute" and never earns the seal.
|
|
353
|
+
const norm = normalizeStreamJson(data);
|
|
354
|
+
if (norm) {
|
|
355
|
+
emit(norm);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
227
358
|
emit({ type: data.type ?? `${cfg.id}_event`, timestamp: Date.now(), data });
|
|
228
359
|
continue;
|
|
229
360
|
}
|
|
@@ -57,7 +57,19 @@ export function buildGraphSync(cwd, { incremental = true } = {}) {
|
|
|
57
57
|
args.push('--update');
|
|
58
58
|
}
|
|
59
59
|
try {
|
|
60
|
-
|
|
60
|
+
// SECURITY (2026-08-08) · `--no-install` is load-bearing, do not drop it.
|
|
61
|
+
// This shelled out to bare `npx graphify-ts`. The dependency is the SCOPED
|
|
62
|
+
// @mohammednagy/graphify-ts, but npx resolves the BIN NAME and falls back to the
|
|
63
|
+
// public registry when it is not installed locally — and an unrelated
|
|
64
|
+
// `graphify-ts` (v0.2.0, publisher "howell5") exists on npm. Reproduced: with the
|
|
65
|
+
// scoped package absent, npx downloaded that stranger's package into
|
|
66
|
+
// ~/.npm/_npx and tried to run it, in the user's repo cwd, with stdio ignored so
|
|
67
|
+
// nothing surfaced, and the catch below swallowed the failure as "degrade
|
|
68
|
+
// gracefully". It only failed to EXECUTE because that package's bin is named
|
|
69
|
+
// `graphify`, which is luck rather than safety. `--no-install` makes npx use a
|
|
70
|
+
// locally-installed bin or fail — never fetch. Dependency confusion is not a
|
|
71
|
+
// thing a product that sells provenance gets to ship.
|
|
72
|
+
execFileSync('npx', ['--no-install', 'graphify-ts', ...args], {
|
|
61
73
|
cwd,
|
|
62
74
|
stdio: 'ignore',
|
|
63
75
|
timeout: 120_000,
|
|
@@ -86,7 +98,7 @@ export function compileContextPackSync(prompt, cwd, { task = 'explain', budget =
|
|
|
86
98
|
return null;
|
|
87
99
|
}
|
|
88
100
|
try {
|
|
89
|
-
const output = execFileSync('npx', ['graphify-ts', 'pack', prompt, '--graph', graphPath, '--task', task, '--budget', String(budget)], {
|
|
101
|
+
const output = execFileSync('npx', ['--no-install', 'graphify-ts', 'pack', prompt, '--graph', graphPath, '--task', task, '--budget', String(budget)], {
|
|
90
102
|
cwd,
|
|
91
103
|
encoding: 'utf-8',
|
|
92
104
|
maxBuffer: 512 * 1024,
|
|
@@ -149,34 +161,6 @@ export function persistContextPack(pack, cwd) {
|
|
|
149
161
|
writeFileSync(packsFile, JSON.stringify(entry) + '\n', { flag: 'a' });
|
|
150
162
|
return packsFile;
|
|
151
163
|
}
|
|
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
164
|
// ── Prompt enrichment (sync) ───────────────────────────────────────────────────
|
|
181
165
|
/**
|
|
182
166
|
* Prepend a context pack to a prompt if graphify-ts is available.
|
|
@@ -222,33 +206,4 @@ export function enrichPromptWithPackSync(prompt, cwd, { force = false } = {}) {
|
|
|
222
206
|
prompt,
|
|
223
207
|
].join('\n\n');
|
|
224
208
|
}
|
|
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
209
|
//# sourceMappingURL=graphify-context.js.map
|
|
@@ -279,8 +279,11 @@ export function buildMemoryPrefix(projectRoot, options = {}) {
|
|
|
279
279
|
*
|
|
280
280
|
* Stops at the filesystem root. Symlink-following on read is the right
|
|
281
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).
|
|
282
285
|
*/
|
|
283
|
-
function findProjectRoot(startPath) {
|
|
286
|
+
export function findProjectRoot(startPath) {
|
|
284
287
|
let current;
|
|
285
288
|
try {
|
|
286
289
|
current = realpathSync(resolve(startPath));
|