Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 28 additions & 24 deletions packages/cli/src/generated/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { join, dirname, basename } from 'node:path';

import { fileURLToPath } from 'node:url';

import { EngineRegistry, loadConfig, resolveWorkingDir, repoRoot, headSha, worktreeCreate, worktreeRemoveBestEffort, resolveDedupSidecar, agonPath } from '@kernlang/agon-core';
import { EngineRegistry, loadConfig, resolveWorkingDir, repoRoot, headSha, worktreeCreate, worktreeRemoveBestEffort, resolveDedupSidecar, resolveSidecarPython, agonPath } from '@kernlang/agon-core';

import { resolveBuiltinEnginesDir } from '../lib/engines-dir.js';

Expand Down Expand Up @@ -192,26 +192,30 @@ export interface PythonDoctorResult {
* Every Python file the bridges actually spawn. Doctor confirms ALL of them are reachable through resolveDedupSidecar — a bad package that ships only some is still a problem.
*/
// @kern-source: doctor:161
export const EXPECTED_SIDECARS: string[] = ['history-search.py','syntax-validator.py','classifier.py','sidecar.py'];
export const EXPECTED_SIDECARS: string[] = ['history-search.py','syntax-validator.py','classifier.py','sidecar.py','embedder.py'];

/**
* Probe whether the Python interpreter exists, every expected sidecar file resolves, and the deps (fastembed, numpy, tree-sitter + grammars) are importable. Fail-soft: not having Python means semantic features fall back, not that agon is broken.
* Return a shell-safe sidecar install command for any working directory, or an empty string when the installer is unavailable.
*/
// @kern-source: doctor:164
export function formatPythonSidecarInstallCommand(installScript: string|null): string {
return installScript
? `node ${shellQuoteForDoctor(installScript)}`
: '';
}

/**
* Probe whether the Python interpreter exists, every expected sidecar file resolves, and the deps (fastembed, numpy, tree-sitter + grammars) are importable. Fail-soft: not having Python means semantic features fall back, not that agon is broken.
*/
// @kern-source: doctor:172
export function diagnoseDedupPython(): PythonDoctorResult {
const python = process.env.AGON_PYTHON || 'python3';
// Tiny probe — imports every dep used across the 4 sidecars in one shot.
const python = resolveSidecarPython();
// Tiny probe — imports every dependency used across the sidecars in one shot.
// Exit 0 = all importable; non-zero stderr names the first missing module.
const probe = 'import fastembed, numpy, tree_sitter, tree_sitter_python, tree_sitter_typescript, tree_sitter_javascript, tree_sitter_json';

// Pick a real path for the install command. requirements.txt ships in
// @kernlang/agon-dedup so we resolve it the same way as the .py files — works in
// both the monorepo and a published install. Falls back to the
// package-name form if resolution somehow fails.
const requirementsPath = resolveDedupSidecar('requirements.txt');
const pipInstall = requirementsPath
? `python3 -m pip install --user -r ${requirementsPath}`
: 'python3 -m pip install --user fastembed numpy tree-sitter tree-sitter-python tree-sitter-typescript tree-sitter-javascript tree-sitter-json';
// Resolve the published installer so the command works from any cwd.
const pipInstall = formatPythonSidecarInstallCommand(resolveDedupSidecar('install-python.mjs'));

let result;
try {
Expand All @@ -223,20 +227,20 @@ export function diagnoseDedupPython(): PythonDoctorResult {
return {
status: 'fail',
detail: `${python} probe threw: ${err instanceof Error ? err.message : String(err)}`,
installCommand: 'Install Python 3.10+ from https://python.org or your package manager, then run the pip install above.',
installCommand: pipInstall || 'Install Python 3.10+ from https://python.org or your package manager.',
};
}
if (result.error || result.status === null) {
const msg = result.error?.message || `${python} not in PATH`;
return {
status: 'fail',
detail: msg,
installCommand: 'Install Python 3.10+ (semantic history, syntax validator, task classifier, brainstorm dedup all degrade without it)',
installCommand: 'Install Python 3.10+ (semantic history, syntax validator, task classifier, brainstorm dedup, and RAG embeddings all degrade without it)',
};
}
if (result.status === 0) {
// Confirm EVERY expected sidecar resolves — a partial dedup package
// would otherwise pass the probe while still leaving 3 of 4 bridges
// would otherwise pass the probe while leaving one or more bridges
// broken at runtime.
const missing = EXPECTED_SIDECARS.filter((name: string) => !resolveDedupSidecar(name));
if (missing.length === EXPECTED_SIDECARS.length) {
Expand All @@ -250,7 +254,7 @@ export function diagnoseDedupPython(): PythonDoctorResult {
return {
status: 'fail',
detail: `@kernlang/agon-dedup is partial — missing sidecar(s): ${missing.join(', ')}`,
installCommand: 'reinstall @kernlang/agon-dedup — the published tarball should include all four .py files',
installCommand: 'reinstall @kernlang/agon-dedup — the published tarball should include all five .py files',
};
}
const sample = resolveDedupSidecar('history-search.py');
Expand All @@ -264,11 +268,11 @@ export function diagnoseDedupPython(): PythonDoctorResult {
return {
status: 'warn',
detail: `${python} OK, but ${missingModule} not installed`,
installCommand: pipInstall,
installCommand: pipInstall || undefined,
};
}

// @kern-source: doctor:236
// @kern-source: doctor:238
export function checkDoctorWorktree(cwd: string): {ok:boolean; message:string; cleanupCommand:string} {
let root = '';
let tempDir = '';
Expand Down Expand Up @@ -305,7 +309,7 @@ export function checkDoctorWorktree(cwd: string): {ok:boolean; message:string; c
/**
* Diagnose the live Cesar harness: selected engine, backend capability, native/MCP session state, and observed tool reliability.
*/
// @kern-source: doctor:270
// @kern-source: doctor:272
export function buildHarnessDoctorReport(registry: EngineRegistry, config: any, cesar?: any): HarnessDoctorReport {
const rows: string[][] = [];
const selected = String((config as any)?.cesarEngine ?? (config as any)?.forgeFixedStarter ?? 'claude');
Expand Down Expand Up @@ -396,14 +400,14 @@ export function buildHarnessDoctorReport(registry: EngineRegistry, config: any,
};
}

// @kern-source: doctor:362
// @kern-source: doctor:364
export interface ReviewDoctorReport {
rows: string[][];
ok: boolean;
summary: string;
}

// @kern-source: doctor:367
// @kern-source: doctor:369
export const REVIEW_DOCTOR_DIFF: string = [
'diff --git a/sample.ts b/sample.ts',
'new file mode 100644',
Expand All @@ -419,7 +423,7 @@ export const REVIEW_DOCTOR_DIFF: string = [
/**
* Review-specific reliability smoke test (#8). For each engine, runs a real review of a tiny synthetic buggy diff through the full runReviewCore pipeline (prompt → dispatch → sentinel parse → repair) under a short hard timeout, then classifies whether the engine produced machine-parseable output. This catches engines that pass `doctor engines` (binary/key reachable) but hang or emit unparseable output in actual review use — the kimi/zai failure mode the user hit.
*/
// @kern-source: doctor:379
// @kern-source: doctor:381
export async function runReviewDoctor(registry: EngineRegistry, config: any, adapter: any, engineIds: string[], timeoutSec: number): Promise<ReviewDoctorReport> {
// Align the inner dispatch timeout with the orchestrator wall clock (+grace)
// so a misbehaving dispatch path that defers abort still can't exceed the
Expand Down Expand Up @@ -475,7 +479,7 @@ export async function runReviewDoctor(registry: EngineRegistry, config: any, ada
};
}

// @kern-source: doctor:436
// @kern-source: doctor:438
export const doctorCommand: any = defineCommand({
meta: {
name: 'doctor',
Expand Down
36 changes: 19 additions & 17 deletions packages/cli/src/kern/commands/doctor.kern
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import from="node:fs" names="mkdtempSync,rmSync,writeFileSync,existsSync"
import from="node:os" names="tmpdir,homedir"
import from="node:path" names="join,dirname,basename"
import from="node:url" names="fileURLToPath"
import from="@kernlang/agon-core" names="EngineRegistry,loadConfig,resolveWorkingDir,repoRoot,headSha,worktreeCreate,worktreeRemoveBestEffort,resolveDedupSidecar,agonPath"
import from="@kernlang/agon-core" names="EngineRegistry,loadConfig,resolveWorkingDir,repoRoot,headSha,worktreeCreate,worktreeRemoveBestEffort,resolveDedupSidecar,resolveSidecarPython,agonPath"
import from="../lib/engines-dir.js" names="resolveBuiltinEnginesDir"
import from="@kernlang/agon-core" names="EngineDefinition" types=true
import from="@kernlang/agon-adapter-cli" names="createCliAdapter"
Expand Down Expand Up @@ -158,25 +158,27 @@ interface name=PythonDoctorResult
field name=detail type=string
field name=installCommand type=string optional=true

const name=EXPECTED_SIDECARS type="string[]" value={{ ['history-search.py','syntax-validator.py','classifier.py','sidecar.py'] }}
const name=EXPECTED_SIDECARS type="string[]" value={{ ['history-search.py','syntax-validator.py','classifier.py','sidecar.py','embedder.py'] }} export=true
doc "Every Python file the bridges actually spawn. Doctor confirms ALL of them are reachable through resolveDedupSidecar — a bad package that ships only some is still a problem."

fn name=formatPythonSidecarInstallCommand params="installScript:string|null" returns=string export=true
doc "Return a shell-safe sidecar install command for any working directory, or an empty string when the installer is unavailable."
handler <<<
return installScript
? `node ${shellQuoteForDoctor(installScript)}`
: '';
>>>

fn name=diagnoseDedupPython returns=PythonDoctorResult export=true
doc "Probe whether the Python interpreter exists, every expected sidecar file resolves, and the deps (fastembed, numpy, tree-sitter + grammars) are importable. Fail-soft: not having Python means semantic features fall back, not that agon is broken."
handler <<<
const python = process.env.AGON_PYTHON || 'python3';
// Tiny probe — imports every dep used across the 4 sidecars in one shot.
const python = resolveSidecarPython();
// Tiny probe — imports every dependency used across the sidecars in one shot.
// Exit 0 = all importable; non-zero stderr names the first missing module.
const probe = 'import fastembed, numpy, tree_sitter, tree_sitter_python, tree_sitter_typescript, tree_sitter_javascript, tree_sitter_json';

// Pick a real path for the install command. requirements.txt ships in
// @kernlang/agon-dedup so we resolve it the same way as the .py files — works in
// both the monorepo and a published install. Falls back to the
// package-name form if resolution somehow fails.
const requirementsPath = resolveDedupSidecar('requirements.txt');
const pipInstall = requirementsPath
? `python3 -m pip install --user -r ${requirementsPath}`
: 'python3 -m pip install --user fastembed numpy tree-sitter tree-sitter-python tree-sitter-typescript tree-sitter-javascript tree-sitter-json';
// Resolve the published installer so the command works from any cwd.
const pipInstall = formatPythonSidecarInstallCommand(resolveDedupSidecar('install-python.mjs'));

let result;
try {
Expand All @@ -188,20 +190,20 @@ fn name=diagnoseDedupPython returns=PythonDoctorResult export=true
return {
status: 'fail',
detail: `${python} probe threw: ${err instanceof Error ? err.message : String(err)}`,
installCommand: 'Install Python 3.10+ from https://python.org or your package manager, then run the pip install above.',
installCommand: pipInstall || 'Install Python 3.10+ from https://python.org or your package manager.',
};
}
if (result.error || result.status === null) {
const msg = result.error?.message || `${python} not in PATH`;
return {
status: 'fail',
detail: msg,
installCommand: 'Install Python 3.10+ (semantic history, syntax validator, task classifier, brainstorm dedup all degrade without it)',
installCommand: 'Install Python 3.10+ (semantic history, syntax validator, task classifier, brainstorm dedup, and RAG embeddings all degrade without it)',
};
}
if (result.status === 0) {
// Confirm EVERY expected sidecar resolves — a partial dedup package
// would otherwise pass the probe while still leaving 3 of 4 bridges
// would otherwise pass the probe while leaving one or more bridges
// broken at runtime.
const missing = EXPECTED_SIDECARS.filter((name: string) => !resolveDedupSidecar(name));
if (missing.length === EXPECTED_SIDECARS.length) {
Expand All @@ -215,7 +217,7 @@ fn name=diagnoseDedupPython returns=PythonDoctorResult export=true
return {
status: 'fail',
detail: `@kernlang/agon-dedup is partial — missing sidecar(s): ${missing.join(', ')}`,
installCommand: 'reinstall @kernlang/agon-dedup — the published tarball should include all four .py files',
installCommand: 'reinstall @kernlang/agon-dedup — the published tarball should include all five .py files',
};
}
const sample = resolveDedupSidecar('history-search.py');
Expand All @@ -229,7 +231,7 @@ fn name=diagnoseDedupPython returns=PythonDoctorResult export=true
return {
status: 'warn',
detail: `${python} OK, but ${missingModule} not installed`,
installCommand: pipInstall,
installCommand: pipInstall || undefined,
};
>>>

Expand Down
18 changes: 16 additions & 2 deletions packages/core/src/generated/blocks/dedup-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// @generated by kern v3.5.7 — DO NOT EDIT. Source: src/kern/blocks/dedup-resolver.kern
// @generated by kern v4.5.0 — DO NOT EDIT. Source: src/kern/blocks/dedup-resolver.kern

import { createRequire } from 'node:module';

Expand All @@ -8,10 +8,24 @@ import { fileURLToPath } from 'node:url';

import { existsSync } from 'node:fs';

import { agonPath } from '../signals/config.js';

/**
* Resolve the Python interpreter for optional sidecars: explicit AGON_PYTHON, then Agon managed virtualenv, then python3.
*/
// @kern-source: dedup-resolver:21
export function resolveSidecarPython(): string {
if (process.env.AGON_PYTHON) return process.env.AGON_PYTHON;
const managed = process.platform === 'win32'
? agonPath('python-sidecar', 'Scripts', 'python.exe')
: agonPath('python-sidecar', 'bin', 'python');
return existsSync(managed) ? managed : 'python3';
}

/**
* Return the absolute path of a Python sidecar shipped in @kernlang/agon-dedup, or null if not found. `filename` is the bare filename (e.g. 'history-search.py'), not a path.
*/
// @kern-source: dedup-resolver:20
// @kern-source: dedup-resolver:31
export function resolveDedupSidecar(filename: string): string | null {
// Mode 1 — production install. require.resolve finds package.json by
// package-name resolution, dirname gives us the install location.
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/generated/blocks/syntax-validator-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { spawnSync } from 'node:child_process';

import { extname } from 'node:path';

import { resolveDedupSidecar } from './dedup-resolver.js';
import { resolveDedupSidecar, resolveSidecarPython } from './dedup-resolver.js';

// @kern-source: syntax-validator-bridge:10
export interface SyntaxValidatorInput {
Expand Down Expand Up @@ -68,7 +68,7 @@ export function validateSyntax(files: SyntaxValidatorInput[]): SyntaxValidatorRe
const sidecar = resolveDedupSidecar('syntax-validator.py');
if (!sidecar) return null;

const python = process.env.AGON_PYTHON || 'python3';
const python = resolveSidecarPython();

let result;
try {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/generated/blocks/task-classifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { TaskClass } from '../models/types.js';

import { spawnSync } from 'node:child_process';

import { resolveDedupSidecar } from './dedup-resolver.js';
import { resolveDedupSidecar, resolveSidecarPython } from './dedup-resolver.js';

// @kern-source: task-classifier:8
export function classifyTaskRegex(description: string): TaskClass {
Expand Down Expand Up @@ -50,7 +50,7 @@ export function classifyTaskSemantic(description: string): TaskClass | null {
const sidecar = resolveDedupSidecar('classifier.py');
if (!sidecar) return null;

const python = process.env.AGON_PYTHON || 'python3';
const python = resolveSidecarPython();

let result;
try {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/generated/rag/embed.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// @generated by kern v4.0.0 — DO NOT EDIT. Source: src/kern/rag/embed.kern
// @generated by kern v4.5.0 — DO NOT EDIT. Source: src/kern/rag/embed.kern

import { spawnSync } from 'node:child_process';

import { resolveDedupSidecar } from '../blocks/dedup-resolver.js';
import { resolveDedupSidecar, resolveSidecarPython } from '../blocks/dedup-resolver.js';

/**
* Generous: the FIRST run downloads the ~30MB MiniLM model into ~/.agon/cache/fastembed. Warm runs take seconds. Override with AGON_RAG_EMBED_TIMEOUT_MS.
Expand Down Expand Up @@ -31,7 +31,7 @@ export function embedTexts(texts: string[]): RagEmbedResult | null {
if (texts.length === 0) return null;
const sidecar = resolveDedupSidecar('embedder.py');
if (!sidecar) return null;
const python = process.env.AGON_PYTHON || 'python3';
const python = resolveSidecarPython();
const timeout = Number(process.env.AGON_RAG_EMBED_TIMEOUT_MS) || RAG_EMBED_TIMEOUT_MS;
const input = texts.map((text, i) => JSON.stringify({ id: String(i), text })).join('\n') + '\n';
let result;
Expand Down
29 changes: 19 additions & 10 deletions packages/core/src/generated/sessions/companion-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,20 +162,27 @@ export async function companionDispatch(opts: {config:CompanionConfig, binaryPat
const alwaysOpt = options.find((o: any) => o.kind === 'allow_always' || o.optionId?.includes('proceed_always'));
const rejectOpt = options.find((o: any) => o.kind === 'reject_once') ?? options.find((o: any) => o.optionId?.includes('reject'));

if (opts.onApproval) {
const respondWithOption = (option: any) => {
if (option?.optionId) {
fireWriteStdin(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result: { optionId: option.optionId } }) + '\n');
} else {
fireWriteStdin(JSON.stringify({ jsonrpc: '2.0', id: msg.id, error: { code: -32001, message: 'Permission denied' } }) + '\n');
}
};

if (opts.mode !== 'agent') {
respondWithOption(rejectOpt);
} else if (opts.onApproval) {
opts.onApproval(String(tName), String(tCmd), 'Agent tool approval requested').then((result: boolean | string) => {
const approved = typeof result === 'string' ? false : result;
const optionId = approved
? (allowOpt?.optionId ?? alwaysOpt?.optionId ?? 'proceed_once')
: (rejectOpt?.optionId ?? 'reject');
fireWriteStdin(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result: { optionId } }) + '\n');
const option = approved ? (allowOpt ?? alwaysOpt) : rejectOpt;
respondWithOption(option);
}).catch(() => {
const optionId = rejectOpt?.optionId ?? 'reject';
fireWriteStdin(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result: { optionId } }) + '\n');
respondWithOption(rejectOpt);
});
} else {
const optionId = allowOpt?.optionId ?? alwaysOpt?.optionId ?? 'proceed_once';
fireWriteStdin(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result: { optionId } }) + '\n');
const option = opts.mode === 'agent' ? (allowOpt ?? alwaysOpt) : rejectOpt;
respondWithOption(option);
}
return;
}
Expand All @@ -190,7 +197,9 @@ export async function companionDispatch(opts: {config:CompanionConfig, binaryPat
const isV2 = m.startsWith('item/');
const buildResult = (approved: boolean) => isV2 ? { decision: approved ? 'accept' : 'decline' } : { approved };

if (opts.onApproval) {
if (opts.mode !== 'agent') {
fireWriteStdin(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result: buildResult(false) }) + '\n');
} else if (opts.onApproval) {
opts.onApproval(String(toolName), String(toolCmd), 'Agent tool approval requested').then((result: boolean | string) => {
if (typeof result === 'string') {
fireWriteStdin(JSON.stringify({ jsonrpc: '2.0', id: msg.id, error: { code: -32001, message: result } }) + '\n');
Expand Down
Loading