codehere 0.8.0 → 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.
Files changed (134) hide show
  1. package/README.md +22 -13
  2. package/dist/application/services/verdict-service.js +154 -0
  3. package/dist/application/use-cases/assess-cross-vendor.js +126 -0
  4. package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
  5. package/dist/application/use-cases/export-aat.js +107 -0
  6. package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
  7. package/dist/application/use-cases/generate-final-report.js +140 -15
  8. package/dist/application/use-cases/generate-narration.js +5 -3
  9. package/dist/application/use-cases/generate-thread-title.js +5 -2
  10. package/dist/application/use-cases/plan-auto-review.js +57 -0
  11. package/dist/application/use-cases/reconcile-audit.js +100 -0
  12. package/dist/application/use-cases/rerun-tests.js +226 -0
  13. package/dist/application/use-cases/search-sessions.js +70 -0
  14. package/dist/application/use-cases/should-auto-review.js +71 -0
  15. package/dist/application/use-cases/thread-export.js +90 -0
  16. package/dist/application/use-cases/verify-claim.js +187 -5
  17. package/dist/domain/audit.js +11 -0
  18. package/dist/domain/services/codebase-detector.js +0 -6
  19. package/dist/domain/task.js +91 -0
  20. package/dist/error-handler.js +3 -115
  21. package/dist/index.js +269 -25
  22. package/dist/infrastructure/completion/bash-completion.js +7 -3
  23. package/dist/infrastructure/completion/fish-completion.js +7 -12
  24. package/dist/infrastructure/completion/zsh-completion.js +7 -12
  25. package/dist/infrastructure/config/config-manager.js +4 -3
  26. package/dist/infrastructure/config/product-context.js +1 -19
  27. package/dist/infrastructure/context/adapters/figma-source.js +37 -2
  28. package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
  29. package/dist/infrastructure/context/github-api.js +110 -0
  30. package/dist/infrastructure/context/github-device-flow.js +65 -0
  31. package/dist/infrastructure/context/oauth-store.js +5 -2
  32. package/dist/infrastructure/context/registry-bootstrap.js +0 -4
  33. package/dist/infrastructure/cost/budget-config.js +3 -2
  34. package/dist/infrastructure/cost/cost-router.js +16 -23
  35. package/dist/infrastructure/cost/spend-aggregator.js +90 -15
  36. package/dist/infrastructure/cost/team-ledger.js +355 -0
  37. package/dist/infrastructure/discovery/discovery-log.js +85 -0
  38. package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
  39. package/dist/infrastructure/hooks/claude-sdk-hook.js +133 -3
  40. package/dist/infrastructure/hooks/hook-mode.js +20 -4
  41. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
  42. package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
  43. package/dist/infrastructure/observability/execution-tracer.js +1 -1
  44. package/dist/infrastructure/orchestration/agents-config-seed.js +22 -10
  45. package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
  46. package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
  47. package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
  48. package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
  49. package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
  50. package/dist/infrastructure/orchestration/context-handoff.js +32 -14
  51. package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
  52. package/dist/infrastructure/orchestration/failover-config.js +0 -10
  53. package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
  54. package/dist/infrastructure/orchestration/generic-cli-adapter.js +132 -1
  55. package/dist/infrastructure/orchestration/graphify-context.js +14 -59
  56. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +164 -9
  57. package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
  58. package/dist/infrastructure/orchestration/progress-aggregator.js +238 -21
  59. package/dist/infrastructure/orchestration/session-manager.js +593 -94
  60. package/dist/infrastructure/orchestration/session-persistence.js +177 -41
  61. package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
  62. package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
  63. package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
  64. package/dist/infrastructure/protocols/mcp-server-impl.js +1462 -380
  65. package/dist/infrastructure/protocols/mcp-tools.js +401 -0
  66. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
  67. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
  68. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
  69. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
  70. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
  71. package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
  72. package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
  73. package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
  74. package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
  75. package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
  76. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
  77. package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
  78. package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
  79. package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
  80. package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
  81. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
  82. package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
  83. package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
  84. package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
  85. package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
  86. package/dist/infrastructure/protocols/ui/styles.js +2147 -0
  87. package/dist/infrastructure/protocols/ui/tokens.js +96 -0
  88. package/dist/infrastructure/protocols/ui-page-v2.js +116 -4214
  89. package/dist/infrastructure/protocols/verify-page.js +849 -155
  90. package/dist/infrastructure/security/fix-code.js +144 -0
  91. package/dist/infrastructure/security/fix-secret.js +89 -0
  92. package/dist/infrastructure/security/permissions-config.js +3 -2
  93. package/dist/infrastructure/security/scan-types.js +10 -0
  94. package/dist/infrastructure/security/ship-scan.js +195 -0
  95. package/dist/infrastructure/security/trust-scorer.js +70 -3
  96. package/dist/infrastructure/security/vibe-checks.js +99 -0
  97. package/dist/infrastructure/storage/atomic-write.js +86 -0
  98. package/dist/infrastructure/storage/project-thread-mirror.js +150 -3
  99. package/dist/infrastructure/storage/project-thread-reader.js +261 -24
  100. package/dist/monitoring.js +2 -2
  101. package/dist/presentation/cli/commands/audit-command.js +79 -4
  102. package/dist/presentation/cli/commands/budget-command.js +2 -2
  103. package/dist/presentation/cli/commands/check-command.js +96 -0
  104. package/dist/presentation/cli/commands/context-source-command.js +20 -1
  105. package/dist/presentation/cli/commands/help-command.js +5 -5
  106. package/dist/presentation/cli/commands/hook-command.js +5 -5
  107. package/dist/presentation/cli/commands/init-command.js +26 -14
  108. package/dist/presentation/cli/commands/memory-command.js +28 -28
  109. package/dist/presentation/cli/commands/permission-command.js +1 -12
  110. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  111. package/dist/presentation/cli/commands/protocols-command.js +4 -4
  112. package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
  113. package/dist/presentation/cli/commands/report-command.js +48 -32
  114. package/dist/presentation/cli/commands/scan-command.js +52 -0
  115. package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
  116. package/dist/presentation/cli/commands/search-command.js +51 -0
  117. package/dist/presentation/cli/commands/setup-command.js +31 -8
  118. package/dist/presentation/cli/commands/status-command.js +39 -86
  119. package/dist/presentation/cli/commands/team-command.js +256 -0
  120. package/dist/presentation/cli/commands/thread-command.js +87 -5
  121. package/dist/presentation/cli/commands/trust-command.js +53 -31
  122. package/dist/presentation/cli/commands/verify-command.js +74 -9
  123. package/dist/presentation/cli/error-display.js +0 -8
  124. package/dist/presentation/cli/repl.js +15 -7
  125. package/dist/presentation/cli/task-deliverable.js +36 -10
  126. package/dist/testing/replay-adapter.js +0 -19
  127. package/dist/ui.js +51 -239
  128. package/dist/utils/version.js +0 -6
  129. package/package.json +30 -10
  130. package/dist/domain/entities/task.js +0 -61
  131. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  132. package/dist/infrastructure/security/input-validator.js +0 -152
  133. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  134. 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
- const followup = spawn('gemini', ['-p', text, '--resume', resumeArg, ...HEADLESS_FLAGS], {
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. 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.",
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
- const hasEnvAuth = !!process.env.GEMINI_API_KEY ||
561
- !!process.env.GOOGLE_API_KEY ||
562
- !!process.env.GOOGLE_GENAI_USE_VERTEXAI ||
563
- !!process.env.GOOGLE_GENAI_USE_GCA ||
564
- !!process.env.GOOGLE_APPLICATION_CREDENTIALS;
565
- const hasFileAuth = existsSync(join(homedir(), '.gemini', 'settings.json')) ||
566
- existsSync(join(homedir(), '.gemini', 'oauth_creds.json'));
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,8 +36,10 @@ 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';
42
+ import { mirrorToolCallToAudit } from '../hooks/mirror-tool-call-to-audit.js';
41
43
  const CONFIG_PATH = join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'agents.yaml');
42
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
@@ -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(cfg.command, [...argv, ...(options.extraArgs ?? [])], {
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
- execFileSync('npx', ['graphify-ts', ...args], {
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